[Spring Boot] 백엔드에서 카카오 로그인 구현하기

박상훈·2024년 4월 11일

프로젝트

목록 보기
1/2

프로젝트 진행 중 카카오 로그인 구현 방법에 대해 공부한 내용을 정리해보자.

1. Kakao Developers 애플리케이션 등록

  • Kakao Developers 접속 후 로그인

  • 내 애플리케이션 등록, 앱 아이콘, 앱 이름, 사업자명 작성

  • 앱 키 탭의 REST API 키와 보안 탭의 Client Secret 코드를 저장

  • 아래와 같이 카카오 로그인을 활성화하고 Redirect URI를 설정한다.

  • 아래와 같이 Web 플랫폼 등록도 진행한다. 로컬에서 테스트할 것이기 때문에 기본 도메인은 Redirect URI와 같이 localhost로 설정한다.

  • 동의 항목 탭에서 가져올 정보들을 선택한다. 대부분의 경우 개인정보 동의항목 심사가 필요하기에 프로젝트에서 사용할 닉네임 정도만 가져오도록 하자.


2. 구현

전체적인 카카오 로그인 과정은 아래와 같다.

front 단에서 아래와 같이 화면에 있는 버튼 클릭 시 인가 코드를 받을 수 있도록 카카오 로그인 화면으로 이동시킨다.

// 카카오 로그인
document.getElementById("kakaoSubmit").addEventListener("click", function () {
  window.location.href = "https://kauth.kakao.com/oauth/authorize?response_type=code&"
  + "client_id={발급받은 REST API 키}&redirect_uri=http://localhost:8080/login/oauth2/code/kakao";
  
}); 

이후 카카오 로그인 화면에서 로그인을 완료하게 되면, 이전에 설정해두었던 redirect uri인 "http://localhost:8080/login/oauth2/code/kakao"로 GET요청을 보내게 된다.

아래는 직접 작성한 컨트롤러이다. redirect uri로 GET 요청을 받았을 경우의 로직을 천천히 살펴보자.

@Controller
@Slf4j
@RequiredArgsConstructor
@RequestMapping(value = "/login/oauth2/code")
public class OAuth2Controller {

    private final SessionLoginService sessionLoginService;
    private final KakaoLoginService kakaoLoginService;

    @Value("${spring.security.oauth2.client.registration.kakao.client-id}")
    private String clientId;

    @Value("${spring.security.oauth2.client.registration.kakao.client-secret}")
    private String clientSecret;

    @GetMapping(value = "/kakao")
    @ResponseBody
    public RedirectView kakaoLogin(@RequestParam String code, HttpServletRequest httpServletRequest) throws JsonProcessingException {

        log.info("code : " + code);

        // 액세스 토큰 발급 받기
        String oAuth2Token = kakaoLoginService.requestToken(code, clientId, clientSecret);

        // 발급받은 토큰으로 회원 정보 가져오기
        Member currentMember = kakaoLoginService.requestMemberInfo(oAuth2Token);

        // 세션 생성
        sessionLoginService.sessionLogin(currentMember, httpServletRequest);

        // 메인 페이지로 리디렉션
        RedirectView redirectView = new RedirectView();
        redirectView.setUrl("http://localhost:5000/mainPage.html");

        return redirectView;
    }
}
  1. Request 파라미터로 code가 넘어오게 되는데, 이것이 바로 전달받은 인가 코드이다. 이후 이 코드를 통해 액세스 토큰을 발급받는다.
  2. 토큰을 발급받으면 발급받은 토큰으로 회원 정보를 조회하여 DB에서 자원을 사용할 수 있다. 위 코드에서는 회원 정보만 가져와 사용했다.
  3. 해당 토큰을 이용해 로그인 상태를 관리해도 되지만, 이미 프로젝트의 로그인을 세션 방식으로 구현 및 진행중이었기에 토큰으로 가져온 회원 정보를 가지고 세션을 생성해 주었다.
  4. 메인 페이지로 리디렉션해준다. 함수의 리턴값에 주의하자.

이번에는 아래의 두 줄에 사용된 함수들의 구현부를 살펴 보자.

 		// 액세스 토큰 발급 받기
        String oAuth2Token = kakaoLoginService.requestToken(code, clientId, clientSecret);

        // 발급받은 토큰으로 회원 정보 가져오기
        Member currentMember = kakaoLoginService.requestMemberInfo(oAuth2Token);
        

kakaoLoginService 클래스의 구현부이다. 인가 코드를 이용해 토큰을 발급받는 함수(requestToken())와 토큰을 이용해 회원 정보를 가져오는 함수(requestMemberInfo())를 직접 작성해준다. 이해하기 쉽도록 함수 내부 곳곳에 주석을 달아두었다.

@Service
@Slf4j
@RequiredArgsConstructor
public class KakaoLoginServiceImpl implements KakaoLoginService {

    private final MemberService memberService;

    public String requestToken(String code, String clientId, String clientSecret) throws JsonProcessingException {
        // 카카오에 POST 방식으로 key=value 데이터를 요청함.
        // RestTemplate 을 사용하면 요청을 편하게 보낼 수 있다.
        RestTemplate rt = new RestTemplate();

        // HttpHeader 오브젝트 생성
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-type",
                "application/x-www-form-urlencoded;charset=utf-8");

        // HttpBody 오브젝트 생성
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code");
        params.add("client_id", clientId);
        params.add("redirect_uri", "http://localhost:8080/login/oauth2/code/kakao");
        params.add("code", code);
        params.add("client_secret", clientSecret);

        // HttpHeader 와 HttpBody 를 하나의 오브젝트에 담기
        HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest = new HttpEntity<>(params, headers);

        // 오브젝트를 담아 Http POST 로 요청하기
        ResponseEntity<String> response = rt.exchange(
                "https://kauth.kakao.com/oauth/token", // 요청 url
                HttpMethod.POST,
                kakaoTokenRequest,
                String.class // 응답받을 타입
        );

        log.info(String.valueOf(response));

        // json 형태로 받은 응답을 자바의 object 형태로 파싱 후 access token 만 가져와 리턴
        // Gson, Json Simple, ObjectMapper 등 사용 가능
        ObjectMapper objectMapper = new ObjectMapper();

        return objectMapper.readTree(response.getBody()).get("access_token").asText();
    }

    @Override
    public Member requestMemberInfo(String token) throws JsonProcessingException {
        // 사용자 정보를 받아오기 위한 또 다른 RestTemplate를 사용해서 응답 받기
        RestTemplate restTemplate = new RestTemplate();

        // HttpHeader 오브젝트 생성
        HttpHeaders httpHeaders = new HttpHeaders();
        //Content-type 을 HttpHeader에 담는다는 것은 내가 담을 데이터가 key-value 데이터라고 알려주는 것이다
        httpHeaders.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
        httpHeaders.add("Authorization","Bearer "+ token);

        //httpBody 생성 부분
        HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest =
                new HttpEntity<>(httpHeaders);
        //Http 요청하기 - POST방식 그리고 response 변수의 응답을 받음
        ResponseEntity<String> responseEntity2 = restTemplate.exchange(
                "https://kapi.kakao.com/v2/user/me",  //요청 주소
                HttpMethod.POST,    //요청방법
                kakaoProfileRequest,  //넘기는 데이터
                String.class    //받아올 데이터 타입
        );

        log.info(String.valueOf(responseEntity2));

        ObjectMapper objectMapper = new ObjectMapper();

        KakaoUserDto kakaoUserDto = new KakaoUserDto(objectMapper.readTree(responseEntity2.getBody()).get("id").asLong(),
                objectMapper.readTree(responseEntity2.getBody()).get("properties").get("nickname").asText());


        return memberService.kakaoLogin(kakaoUserDto);
    }
}

3. 결론

  • 소셜로그인 구현에 대해 공부해보았는데, 로그인 과정 중 토큰 값을 가져오는것을 알았더라면 프로젝트 처음부터 세션방식이 아닌 토큰방식을 채택할 걸 그랬다.
  • api 테스터를 이용해 테스팅을 하며 프론트단에서의 결과와 비교하며 구현하는 것이 편했다. api 테스터를 앞으로도 많이 활용할 것
  • 개인적으로 백엔드까지 갈 필요 없이 토큰 발급까지는 프론트단에서 처리하는 것이 편할 것 같다.
profile
안녕하세요

0개의 댓글