관리 메뉴

거니의 velog

(40) 리액트 소셜 로그인 3 본문

SpringBoot_React 풀스택 프로젝트

(40) 리액트 소셜 로그인 3

Unlimited00 2024. 3. 7. 18:45

4. API 서버에서 Access Token 처리

* 예제에서는 프론트 환경에서 Access Token까지 처리하고 API 서버에 이를 전달해서 API 서버 내에서 사용자와 관련된 처리를 하는 방식으로 구성할 것이다. 따라서, API 서버에 추가적인 기능을 개발해야 한다.
* 카카오 서비스에서 사용자 정보를 가져오기 위해서는 https://kapi.kakao.com/v2/user/me 를 Access Token을 이용해서 호출해야 한다.
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com


(1) MemberService의 개발

* API 서버에서는 Access Token으로 기존의 회원정보를 이용하거나 새로운 회원으로 추가할 것이므로 서비스 계층을 만들어서 처리한다. service 패키지에는 MemberService/MemberServiceImpl을 추가하고 accessToken을 파라미터로 받아서 로그인 처리에 사용하는 MemerDTO를 반환하는 getKakaoMember() 를 추가한다.

package com.unlimited.mallapi.service;

import org.springframework.transaction.annotation.Transactional;

import com.unlimited.mallapi.dto.MemberDTO;

@Transactional(rollbackFor = Exception.class)
public interface MemberService {

    MemberDTO getKakaoMember(String accessToken);

}

* MemberServiceImpl 에서는 RestTemplate 을 이용해서 카카오 서비스를 호출한다. 호출 결과는 Map 타입으로 나오는데 이 중에서 이메일 주소를 추출한다.

package com.unlimited.mallapi.service;

import java.util.LinkedHashMap;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import com.unlimited.mallapi.dto.MemberDTO;
import com.unlimited.mallapi.repository.MemberRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Service
@RequiredArgsConstructor
@Log4j2
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    /**
     * Kakao Access Token을 이용하여 Kakao 사용자 정보를 가져오는 메서드
     *
     * @param accessToken Kakao API에 접근하기 위한 Access Token
     * @return Kakao 사용자 정보에 대한 DTO 객체
     */
    @Override
    public MemberDTO getKakaoMember(String accessToken) {
        // Kakao API를 통해 사용자 이메일 정보를 얻어옴
        String email = getEmailFromKakaoAccessToken(accessToken);

        log.info("Kakao 사용자 이메일: " + email);

        // 추후에 사용자 정보를 처리하는 로직이 구현되면 해당 정보를 담은 DTO를 반환
        return null;
    }

    /**
     * Kakao Access Token을 이용하여 Kakao API에 요청하여 사용자 이메일 정보를 가져오는 메서드
     *
     * @param accessToken Kakao API에 접근하기 위한 Access Token
     * @return Kakao 사용자 이메일
     */
    private String getEmailFromKakaoAccessToken(String accessToken) {
        // Kakao API에서 사용자 정보를 가져오는 엔드포인트 URL
        String kakaoGetUserURL = "https://kapi.kakao.com/v2/user/me";

        // Access Token이 null일 경우 예외 발생
        if (accessToken == null) {
            throw new RuntimeException("Access Token이 null입니다.");
        }

        // RestTemplate을 사용하여 Kakao API에 요청을 보내고 응답을 받아옴
        RestTemplate restTemplate = new RestTemplate();

        // HTTP 요청 헤더 설정
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + accessToken);
        headers.add("Content-Type", "application/x-www-form-urlencoded");
        HttpEntity<String> entity = new HttpEntity<>(headers);

        // Kakao API 엔드포인트 URL을 사용하여 UriComponents 생성
        UriComponents uriBuilder = UriComponentsBuilder.fromHttpUrl(kakaoGetUserURL).build();

        // RestTemplate을 사용하여 Kakao API에 GET 요청을 보내고 응답을 받아옴
        ResponseEntity<LinkedHashMap> response = restTemplate.exchange(
                uriBuilder.toString(),
                HttpMethod.GET,
                entity,
                LinkedHashMap.class);

        // 응답 내용을 로깅
        log.info("Kakao API 응답: " + response);

        // 응답 내용에서 사용자 정보를 추출
        LinkedHashMap<String, LinkedHashMap> bodyMap = response.getBody();

        // 로깅
        log.info("------------------------------------");
        log.info("Kakao API 응답 내용: " + bodyMap);

        // 응답 내용에서 Kakao 계정 정보를 추출
        LinkedHashMap<String, String> kakaoAccount = bodyMap.get("kakao_account");

        // 로깅
        log.info("Kakao 계정 정보: " + kakaoAccount);

        // Kakao 계정 정보에서 이메일을 추출하여 반환
        return kakaoAccount.get("email");
    }
}

(2) SocialController의 개발

* MemberService의 호출은 SocialController로 처리한다. 우선은 MemberService의 동작을 확인하는 목적으로 문자열의 배열과 같이 의미가 없는 결과를 반환하고 MemberService의 getMemberFromKakao() 를 호출하도록 작성한다.

package com.unlimited.mallapi.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.unlimited.mallapi.service.MemberService;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@RestController
@Log4j2
@RequiredArgsConstructor
public class SocialController {

    private final MemberService memberService;

    /**
     * Kakao Access Token을 이용하여 Kakao 사용자 정보를 가져오는 엔드포인트
     *
     * @param accessToken Kakao API에 접근하기 위한 Access Token
     * @return 사용자 정보를 담은 배열 또는 메시지 (현재는 더미 데이터 반환)
     */
    @GetMapping("/api/member/kakao")
    public String[] getMemberFromKakao(@RequestParam("accessToken") String accessToken) {
        log.info("Kakao 사용자 정보 조회를 위한 엔드포인트 호출");
        log.info("전달받은 Access Token: " + accessToken);

        // Kakao 사용자 정보 조회 서비스 호출
        memberService.getKakaoMember(accessToken);

        // 현재는 더미 데이터를 반환
        return new String[] { "AAA", "BBB", "CCC" };
    }
}

(3) 리액트의 호출 테스트

* 리액트에서는 인가 코드를 이용해서 API 서버를 호출하는 기능을 kakaoApi.js 내 getMemberWithAccessToken() 으로 추가한다.

import axios from "axios";
import { API_SERVER_HOST } from "./todoApi";

//const rest_api_key = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`; // REST키값
const redirect_uri = `http://localhost:3000/member/kakao`;

const auth_code_path = `https://kauth.kakao.com/oauth/authorize`;
const access_token_url = `https://kauth.kakao.com/oauth/token`; // 추가

export const getKakaoLoginLink = () => {
  const kakaoURL = `${auth_code_path}?client_id=${rest_api_key}&redirect_uri=${redirect_uri}&response_type=code`;
  return kakaoURL;
};

export const getAccessToken = async (authCode) => {
  const header = {
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
  };
  const params = {
    grant_type: "authorization_code",
    client_id: rest_api_key,
    redirect_uri: redirect_uri,
    code: authCode,
  };
  const res = await axios.post(access_token_url, params, header);
  const accessToken = res.data.access_token;
  return accessToken;
};

export const getMemberWithAccessToken = async (accessToken) => {
  const res = await axios.get(
    `${API_SERVER_HOST}/api/member/kakao?accessToken=${accessToken}`
  );
  return res.data;
};

* KakaoRedirectPage 에서는 Access Token을 받은 후 getMemberWithAccessToken()을 호출하도록 수정한다.

import React, { useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import { getAccessToken, getMemberWithAccessToken } from "../../api/kakaoApi";

const KakaoRedirectPage = () => {
  const [searchParams] = useSearchParams();
  const authCode = searchParams.get("code");

  useEffect(() => {
    getAccessToken(authCode).then((accessToken) => {
      console.log(accessToken);

      getMemberWithAccessToken(accessToken).then((memberInfo) => {
        console.log("-------------------");
        console.log(memberInfo);
      });
    });
  }, [authCode]);

  return (
    <div>
      <div>Kakao Login Redirect</div>
      <div>{authCode}</div>
    </div>
  );
};

export default KakaoRedirectPage;

* API 서버에서는 리액트에서 카카오 로그인을 수행하고 나면 Access Token이 API 서버에 전달되어서 처리되는 로그들을 확인할 수 있다.