본문 바로가기
컴퓨터 프로그래밍/Spring

[Spring] OAuth2.0 Kakao 소셜 로그인 기능 구현

by 한33 2024. 9. 29.

준비 단계

1. Kakao Developer 에서 어플리케이션을 생성하고 Web 플랫폼 등록 에서 사이트 도메인을 입력해준다.

 

2. 카카오 로그인을 했을 때 인가코드를 전달 받을 Redirect URI ( callback ) 을 설정해준다.

 

 

인가코드를 전달받을 Controller 의 요청으로 보내주면 된다.


3. 사용자로부터 받을 정보를 선택해준다.

 


 

https://kauth.kakao.com/oauth/authorize?client_id={REST_API_KEY}&redirect_uri={REDIRECT_URI}&response_type=code

 

인가코드를 받기 위해서 해당 REST_API_KEY 와 Redirect_URI 를 해당 애플리케이션과 callback 설정한 uri 를 맞춰서 넣어준다.

 

RestTemplateConfig

@Configuration
public class RestTemplateConfig  {
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder
                // RestTemplate 으로 외부 API 호출 시 일정 시간이 지나도 응답이 없을 때
                // 무한 대기 상태 방지를 위해 강제 종료 설정
                .setConnectTimeout(Duration.ofSeconds(5)) // 5초
                .setReadTimeout(Duration.ofSeconds(5)) // 5초
                .build();
    }
}

 

RestTemplate 을 사용하기 위해 RestTemplateConfig 를 만들어주고,


KakaoUserController

@GetMapping("/login-kakao")
public String kakaoLogin(@RequestParam("code") String code, HttpServletResponse response) throws JsonProcessingException {
    // code: 카카오 서버로부터 받은 인가 코드
    String token  = kakaoUserService.kakaoLogin(code, response);
    // Cookie 생성 및 직접 브라우저에 Set
    Cookie cookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER, token.substring(7));
    cookie.setPath("/");
    response.addCookie(cookie);
    
    return "main";
}

 

전달받은 인가코드를 code 변수로 전달받고 HttpServletResponse 와 함께 kakaoUserService.kakaoLogin 으로 전달시킨다.


KakaoUserService

@Slf4j(topic = "KAKAO Login")
@Service
@RequiredArgsConstructor
public class KakaoUserService {

    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;
    private final RestTemplate restTemplate;
    private final JwtUtil jwtUtil;

    public String kakaoLogin(String code, HttpServletResponse response) throws JsonProcessingException {

        // 1. "인가 코드"로 "액세스 토큰" 요청
        String accessToken = getToken(code);

        // 2. 토큰으로 카카오 API 호출 : "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
        KakaoUserInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken);

        // 3. 필요시에 회원가입
        User kakaoUser = registerKakaoUserIfNeeded(kakaoUserInfo);

        // 4. JWT 토큰 반환
        String createToken =  jwtUtil.createToken(kakaoUser.getId());

        log.info(createToken);

        return createToken;
//        return accessToken;
    }
private String getToken(String code) throws JsonProcessingException {
    // 요청 URL 만들기
    URI uri = UriComponentsBuilder
            .fromUriString("https://kauth.kakao.com")
            .path("/oauth/token")
            .encode()
            .build()
            .toUri();

    // HTTP Header 생성
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

    // HTTP Body 생성
    MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
    body.add("grant_type", "authorization_code");
    body.add("client_id", "aeeffb48d21d2171981162a755a54141");
    body.add("redirect_uri", "http://localhost:8080/login-kakao");
    body.add("code", code);

    RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
            .post(uri)
            .headers(headers)
            .body(body);

    // HTTP 요청 보내기
    ResponseEntity<String> response = restTemplate.exchange(
            requestEntity,
            String.class
    );

    // HTTP 응답 (JSON) -> 액세스 토큰 파싱
    JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
    return jsonNode.get("access_token").asText();
}

 

kakao 로 인가코드를 보내서 액세스 토큰을 요청해 받아왔다.

 

Http Body 를 생성하는 쪽에서 보면

MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", "aeeffb48d21d2171981162a755a54141");
body.add("redirect_uri", "http://localhost:8080/login-kakao");
body.add("code", code);

 

client_id 와 redirect_uri 를 알맞게 수정해주어야 한다.


private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
    // 요청 URL 만들기
    URI uri = UriComponentsBuilder
            .fromUriString("https://kapi.kakao.com")
            .path("/v2/user/me")
            .encode()
            .build()
            .toUri();

    // HTTP Header 생성
    HttpHeaders headers = new HttpHeaders();
    headers.add("Authorization", "Bearer " + accessToken);
    headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

    RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
            .post(uri)
            .headers(headers)
            .body(new LinkedMultiValueMap<>());

    // HTTP 요청 보내기
    ResponseEntity<String> response = restTemplate.exchange(
            requestEntity,
            String.class
    );


    JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
    Long id = jsonNode.get("id").asLong();
    String nickname = jsonNode.get("properties")
            .get("nickname").asText();
    String email = jsonNode.get("kakao_account")
            .get("email").asText();

    log.info("카카오 사용자 정보: " + id + ", " + nickname + ", " + email);
    return new KakaoUserInfoDto(id, nickname, email);
}

 

받아온 액세스토큰으로 카카오 사용자 정보를 불러온다.

 

현재는 id 로부터 nickname 과 email 을 불러오기 때문에 추가 정보가 필요하다면 이 부분을 수정해야겠다.


private User registerKakaoUserIfNeeded(KakaoUserInfoDto kakaoUserInfo) {
    // DB 에 중복된 Kakao Id 가 있는지 확인
    Long kakaoId = kakaoUserInfo.getId();
    User kakaoUser = userRepository.findByKakaoId(kakaoId).orElse(null);

    if (kakaoUser == null) {
        // 카카오 사용자 email 동일한 email 가진 회원이 있는지 확인
        String kakaoEmail = kakaoUserInfo.getEmail();

        User sameEmailUser = userRepository.findByEmail(kakaoEmail).orElse(null);
        if (sameEmailUser != null) {
            kakaoUser = sameEmailUser;
            // 기존 회원정보에 카카오 Id 추가
            kakaoUser = kakaoUser.kakaoIdUpdate(kakaoId);
        } else {
            // 신규 회원가입
            // password: random UUID
            String password = UUID.randomUUID().toString();
            String encodedPassword = passwordEncoder.encode(password);

            // email: kakao email
            String email = kakaoUserInfo.getEmail();

            kakaoUser = new User(email, kakaoUserInfo.getNickname(), encodedPassword, UserRole.GUEST, UserStatus.ACTIVE, kakaoId);
        }

        userRepository.save(kakaoUser);
    }
    return kakaoUser;
}

 

이미 기존에 등록되어있는 회원이 있다면 그대로 로그인을 진행하고,

 

회원이 없다면 신규등록으로 user 객체를 만들었다.


전체 코드

더보기
package com.sparta.sweethoney.domain.user.kakaoUser.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.sweethoney.config.PasswordEncoder;
import com.sparta.sweethoney.domain.common.dto.AuthUser;
import com.sparta.sweethoney.domain.common.exception.user.NotFoundUserException;
import com.sparta.sweethoney.domain.user.entity.User;
import com.sparta.sweethoney.domain.user.entity.UserRole;
import com.sparta.sweethoney.domain.user.entity.UserStatus;
import com.sparta.sweethoney.domain.user.kakaoUser.dto.KakaoRegisterRequestDto;
import com.sparta.sweethoney.domain.user.kakaoUser.dto.KakaoUserInfoDto;
import com.sparta.sweethoney.domain.user.repository.UserRepository;
import com.sparta.sweethoney.util.JwtUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.util.UUID;

@Slf4j(topic = "KAKAO Login")
@Service
@RequiredArgsConstructor
public class KakaoUserService {

    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;
    private final RestTemplate restTemplate;
    private final JwtUtil jwtUtil;

    public String kakaoLogin(String code, HttpServletResponse response) throws JsonProcessingException {

        // 1. "인가 코드"로 "액세스 토큰" 요청
        String accessToken = getToken(code);

        // 2. 토큰으로 카카오 API 호출 : "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
        KakaoUserInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken);

        // 3. 필요시에 회원가입
        User kakaoUser = registerKakaoUserIfNeeded(kakaoUserInfo);

        // 4. JWT 토큰 반환
        String createToken = jwtUtil.createToken(kakaoUser.getId());

        log.info(createToken);

        return createToken;
//        return accessToken;
    }

    private String getToken(String code) throws JsonProcessingException {
        // 요청 URL 만들기
        URI uri = UriComponentsBuilder
                .fromUriString("https://kauth.kakao.com")
                .path("/oauth/token")
                .encode()
                .build()
                .toUri();

        // HTTP Header 생성
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        // HTTP Body 생성
        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "authorization_code");
        body.add("client_id", "aeeffb48d21d2171981162a755a54141");
        body.add("redirect_uri", "http://localhost:8080/login-kakao");
        body.add("code", code);

        RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
                .post(uri)
                .headers(headers)
                .body(body);

        // HTTP 요청 보내기
        ResponseEntity<String> response = restTemplate.exchange(
                requestEntity,
                String.class
        );

        // HTTP 응답 (JSON) -> 액세스 토큰 파싱
        JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
        return jsonNode.get("access_token").asText();
    }

    private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
        // 요청 URL 만들기
        URI uri = UriComponentsBuilder
                .fromUriString("https://kapi.kakao.com")
                .path("/v2/user/me")
                .encode()
                .build()
                .toUri();

        // HTTP Header 생성
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + accessToken);
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
                .post(uri)
                .headers(headers)
                .body(new LinkedMultiValueMap<>());

        // HTTP 요청 보내기
        ResponseEntity<String> response = restTemplate.exchange(
                requestEntity,
                String.class
        );


        JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
        Long id = jsonNode.get("id").asLong();
        String nickname = jsonNode.get("properties")
                .get("nickname").asText();
        String email = jsonNode.get("kakao_account")
                .get("email").asText();

        log.info("카카오 사용자 정보: " + id + ", " + nickname + ", " + email);
        return new KakaoUserInfoDto(id, nickname, email);
    }

    private User registerKakaoUserIfNeeded(KakaoUserInfoDto kakaoUserInfo) {
        // DB 에 중복된 Kakao Id 가 있는지 확인
        Long kakaoId = kakaoUserInfo.getId();
        User kakaoUser = userRepository.findByKakaoId(kakaoId).orElse(null);

        if (kakaoUser == null) {
            // 카카오 사용자 email 동일한 email 가진 회원이 있는지 확인
            String kakaoEmail = kakaoUserInfo.getEmail();

            User sameEmailUser = userRepository.findByEmail(kakaoEmail).orElse(null);
            if (sameEmailUser != null) {
                kakaoUser = sameEmailUser;
                // 기존 회원정보에 카카오 Id 추가
                kakaoUser = kakaoUser.kakaoIdUpdate(kakaoId);
            } else {
                // 신규 회원가입
                // password: random UUID
                String password = UUID.randomUUID().toString();
                String encodedPassword = passwordEncoder.encode(password);

                // email: kakao email
                String email = kakaoUserInfo.getEmail();

                kakaoUser = new User(email, kakaoUserInfo.getNickname(), encodedPassword, UserRole.GUEST, UserStatus.ACTIVE, kakaoId);
            }

            userRepository.save(kakaoUser);
        }
        return kakaoUser;
    }
}