Spring/OAuth2

[스프링 부트] Kakao 로그인 API 연결

동그리담 2024. 6. 20. 23:21

Kakao Developers에 내 어플리케이션에 등록후

[앱 설정] -> 일반에 들어가면 테스트앱 추가가 나온다.

테스트 앱으로 가서

카카오 로그인을 활성화 시키고

그 아래 항목인 보안에 가서 보안을 활성화 시킨다.

활성화 시키면 나오는 키는 REST API 키와 동일하다.

 

구글, 네이버 등 다른 OAuth2 로그인 방식 확장을 위해 필자는 클래스를 정의해서 송신하는 방법을 택했다.

@Service
public class OAuth2LoginService {
    private final UserJoinDao userJoinDao; //회원가입 Dao
    private final JwtTokenUtil jwtTokenUtil; //JWT Util
    private final UserLoginDao userLoginDao; //로그인 Dao
    private final RestTemplate restTemplate; //API 송신을 위한 주입

    @Autowired
    public OAuth2LoginService(UserJoinDao userJoinDao, JwtTokenUtil jwtTokenUtil, UserLoginDao userLoginDao, RestTemplate restTemplate) {
        this.userJoinDao = userJoinDao;
        this.jwtTokenUtil = jwtTokenUtil;
        this.userLoginDao = userLoginDao;
        this.restTemplate = restTemplate;
    }

    @Value("${kakao.js.key}")
    private String KAKAO_CLIENT_ID;

    @Value("${kakao.restapi.key}")
    private String KAKAO_CLIENT_SECRET;

    @Value("${kakao.request.uri}")
    private String KAKAO_REDIRECT_URL;

    private final static String KAKAO_AUTH_URI = "https://kauth.kakao.com";
    private final static String KAKAO_API_URI = "https://kapi.kakao.com";

    public String getKakaoLogin() {
        return KAKAO_AUTH_URI + "/oauth/authorize"
                + "?client_id=" + KAKAO_CLIENT_ID
                + "&redirect_uri=" + KAKAO_REDIRECT_URL
                + "&response_type=code";
    }
}

 

해당 프로젝트에서 목표하는 바는 회원 정보가 없을 시에 카카오계정으로 회원가입까지 진행시키기 위해 필요 메서드들을 더 추가 하였다.

package com.trioshop.service.user;

import com.trioshop.model.dto.user.KakaoDTO;
import com.trioshop.model.dto.user.Oauth2JoinModel;
import com.trioshop.model.dto.user.UserInfoBySession;
import com.trioshop.model.dto.user.UsersInfoEntity;
import com.trioshop.repository.dao.user.UserJoinDao;
import com.trioshop.repository.dao.user.UserLoginDao;
import com.trioshop.utils.handler.LoginSuccessHandler;
import com.trioshop.utils.service.JwtTokenUtil;
import jakarta.servlet.http.HttpServletResponse;

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;

@Service
public class OAuth2LoginService {
    private final UserJoinDao userJoinDao;
    private final JwtTokenUtil jwtTokenUtil;
    private final UserLoginDao userLoginDao;
    private final RestTemplate restTemplate;

    @Autowired
    public OAuth2LoginService(UserJoinDao userJoinDao, JwtTokenUtil jwtTokenUtil, UserLoginDao userLoginDao, RestTemplate restTemplate) {
        this.userJoinDao = userJoinDao;
        this.jwtTokenUtil = jwtTokenUtil;
        this.userLoginDao = userLoginDao;
        this.restTemplate = restTemplate;
    }

    @Value("${kakao.js.key}")
    private String KAKAO_CLIENT_ID;

    @Value("${kakao.restapi.key}")
    private String KAKAO_CLIENT_SECRET;

    @Value("${kakao.request.uri}")
    private String KAKAO_REDIRECT_URL;

    private final static String KAKAO_AUTH_URI = "https://kauth.kakao.com";
    private final static String KAKAO_API_URI = "https://kapi.kakao.com";

    public String getKakaoLogin() {
        return KAKAO_AUTH_URI + "/oauth/authorize"
                + "?client_id=" + KAKAO_CLIENT_ID
                + "&redirect_uri=" + KAKAO_REDIRECT_URL
                + "&response_type=code";
    }

    public void getKakaoInfo(String code, HttpServletResponse cookieResponse) throws IOException {
        if (code == null) {
            throw new RuntimeException("Failed to get authorization code");
        }

        String accessToken;
        String refreshToken;

        try {
            HttpHeaders headers = new HttpHeaders();
            headers.add("Content-type", "application/x-www-form-urlencoded");

            MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
            params.add("grant_type", "authorization_code");
            params.add("client_id", KAKAO_CLIENT_ID);
            params.add("client_secret", KAKAO_CLIENT_SECRET);
            params.add("code", code);
            params.add("redirect_uri", KAKAO_REDIRECT_URL);

            HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);
            ResponseEntity<String> response = restTemplate.exchange(
                    KAKAO_AUTH_URI + "/oauth/token",
                    HttpMethod.POST,
                    httpEntity,
                    String.class
            );

            JSONParser jsonParser = new JSONParser();
            JSONObject jsonObj = (JSONObject) jsonParser.parse(response.getBody());

            accessToken = (String) jsonObj.get("access_token");
            refreshToken = (String) jsonObj.get("refresh_token");
        } catch (Exception e) {
            throw new RuntimeException("Failed to obtain access token from Kakao API", e);
        }

        KakaoDTO userInfoWithToken = getUserInfoWithToken(accessToken, refreshToken);
        UserInfoBySession userInfoBySession = userLoginDao.loadUserByUsername(userInfoWithToken.getEmail());
        if (userInfoBySession == null) {
            saveOAuthInfo(userInfoWithToken);
            userInfoBySession = userLoginDao.loadUserByUsername(userInfoWithToken.getEmail());
        }

        oauthUserJwtToken(cookieResponse, userInfoBySession);
    }

    private KakaoDTO getUserInfoWithToken(String accessToken, String refreshToken) {
        try {
            HttpHeaders headers = new HttpHeaders();
            headers.add("Authorization", "Bearer " + accessToken);
            headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

            HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(headers);
            ResponseEntity<String> response = restTemplate.exchange(
                    KAKAO_API_URI + "/v2/user/me",
                    HttpMethod.POST,
                    httpEntity,
                    String.class
            );

            JSONParser jsonParser = new JSONParser();
            JSONObject jsonObj = (JSONObject) jsonParser.parse(response.getBody());
            JSONObject account = (JSONObject) jsonObj.get("kakao_account");
            JSONObject profile = (JSONObject) account.get("profile");


            String email = String.valueOf(account.get("email"));
            String nickname = String.valueOf(profile.get("nickname"));

            return KakaoDTO.builder()
                    .email(email)
                    .nickname(nickname).build();
        } catch (Exception e) {
            if (e.getMessage().contains("access token expired")) {
                return refreshAccessTokenAndRetry(refreshToken);
            }
            throw new RuntimeException("Failed to get user info from Kakao API", e);
        }
    }

    private KakaoDTO refreshAccessTokenAndRetry(String refreshToken) {
        try {
            HttpHeaders headers = new HttpHeaders();
            headers.add("Content-type", "application/x-www-form-urlencoded");

            MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
            params.add("grant_type", "refresh_token");
            params.add("client_id", KAKAO_CLIENT_ID);
            params.add("refresh_token", refreshToken);
            params.add("client_secret", KAKAO_CLIENT_SECRET);

            HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);
            ResponseEntity<String> response = restTemplate.exchange(
                    KAKAO_AUTH_URI + "/oauth/token",
                    HttpMethod.POST,
                    httpEntity,
                    String.class
            );

            JSONParser jsonParser = new JSONParser();
            JSONObject jsonObj = (JSONObject) jsonParser.parse(response.getBody());

            String newAccessToken = (String) jsonObj.get("access_token");

            return getUserInfoWithToken(newAccessToken, refreshToken);
        } catch (Exception e) {
            throw new RuntimeException("Failed to refresh access token", e);
        }
    }

    private void oauthUserJwtToken(HttpServletResponse response, UserInfoBySession oauthUser) throws IOException {
        LoginSuccessHandler login = new LoginSuccessHandler(jwtTokenUtil);
        login.loginSuccess(response, oauthUser);
    }

    private void saveOAuthInfo(KakaoDTO kakaoDTO) {
        Long userCode = userJoinDao.insertUsersData(new Oauth2JoinModel(kakaoDTO.getEmail(), "1"));
        userJoinDao.insertUsersInfoData(new UsersInfoEntity(userCode,
                kakaoDTO.getNickname(),
                "010-0000-0000",
                kakaoDTO.getNickname()
        ));
    }
}

 

아까 발급 받았던 Private Key들은 Properties에 정의해서 클래스 내에서는 주입 받아서 사용한다.

application.properties

kakao.js.key={JavaScript Key}
kakao.restapi.key= {REST API Key}
kakao.admin.key = {Admin Key}
kakao.request.uri=https://{URL}/login/oauth/kakao