Ezcho

[OAuth2.0] Spring + OAuth2.0 Kakao 소셜 로그인 구현(1) 본문

BE/API

[OAuth2.0] Spring + OAuth2.0 Kakao 소셜 로그인 구현(1)

Ezcho 2024. 1. 2. 19:08

이전글

https://ezerocho.tistory.com/61

 

[OAuth 2.0] OAuth2.0 프로토콜 동작과정

OAuth2.0, Open Authorization 2.0은 인증을 위한 개방형 표준 프로토콜 이다. 흔히들 소셜로그인이라 부르는 카카오톡로그인, Google로그인, 네이버로그인등 타서비스의 계정을 사용해 내가 개발한 서비

ezerocho.tistory.com

 

 

1. 카카오 Developers에 내 서비스 추가하기


 

https://developers.kakao.com/console/app

 

카카오계정

 

accounts.kakao.com

위 페이지에 접속하여 로그인을 한 후 애플리케이션을 하나 추가해준다.

그리고 아래 페이지에 따라 웹애플리케이션을 가동시키기 위한 설정들을 해주자.

 

https://imweb.me/faq?mode=view&category=29&category2=47&idx=71729

 

카카오 로그인 설정하기

중요: 같은 이름(닉네임)으로 중복 가입 방지 옵션을 해제해 주세요. 소셜 로그인 가입자는 대부분 가입된 플랫폼의 이름(닉네임)을 그대로 사용하는 경향이 있으며, 소셜 로그인 플랫

imweb.me

 

위 과정을 따르고나면 아래와 같은 정보들을 받아올 수 있는데, 복사해서 메모장과 같은곳에 일단 저장해두자.

REST API 키, client id: cc5e87a764c5433be4d533a6ec97a0263

secreat id: VYnj0CXiANqBtXFBU5dCfpe8QXR7LORfJ

Redirect URI: https://localhost:8080/kakao/redirect(로컬인경우, 혹은 본인 배포 서비스 domain을 포함함)

 

 

 

 

 

2. application properties, 프로퍼티 작성


REST API 키, client id: cc5e87764c5433be4d513a6ec97a0263

secreat id: VYnjFCXiANqBtXFBU5dfpe8CXR7LORfJ

Redirect URI: https://localhost:8080/kakao/redirect

 

로그인 설정을 한 후 위 세가지 정보를 가지고 개발을 할것이다. 본인만의 값들이 있으니 참고하고 미리저장해두자.

 

https://developers.kakao.com/docs/latest/ko/kakaologin/common

 

Kakao Developers

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

developers.kakao.com

위 문서를 활용하여 작성되었음을 참고하고, 카카오 로그인 관련 문서를 먼저 읽어보길 추천한다.

 

spring.security.oauth2.client.registration.kakao.client-id=cc5e8sdaf7764c5433be4d533a6ec97a0263
spring.security.oauth2.client.registration.kakao.redirect_uri=http://localhost:8080/kakao/redirect
spring.security.oauth2.client.registration.kakao.client-secret=VCYanjdsaf0CXiANqBtXFBU5dfpe8QXR7LORfJ
spring.security.oauth2.client.registration.kakao.scope=profile_nickname, profile_image, profile_email

우선 application.properties 혹은 yml에 위의 세 정보를 위와같은 양식으로 작성해준다. 이후 이 값들은 서비스클래스에서 @Value를 통해 가져와 사용할것이다.

 

 

3. Auth코드 받기(인가 코드 받기)


우선 문서를 살펴보면 아래와 같은 GET요청을 보내야 함을 알수있다.

Request

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

 

 

우선 서비스부터 작성해주자.

@Service
public class KakaoService {
    @Autowired
    private MemberRepository memberRepository;

    @Value("${spring.security.oauth2.client.registration.kakao.client-id}")
    private String KAKAO_CLIENT_ID;
    @Value("${spring.security.oauth2.client.registration.kakao.client-secret}")
    private String KAKAO_CLIENT_SECRET;
    @Value("${spring.security.oauth2.client.registration.kakao.redirect_uri}")
    private String KAKAO_REDIRECT_URL;
    
    
    private final static String KAKAO_AUTH_URI = "https://kauth.kakao.com";
    //Authorization 코드 받기 위한 도메인
    private final static String KAKAO_API_URI = "https://kapi.kakao.com";

}

 

서비스를 하나 만들어주고 우리가 프로퍼티에 작성했던 값들을 불러와서 private 변수로 선언해준다. 그리고 편의를위해 관련 url 도 변수화해준다.

 

위에서 작성한 요청을 보내보자.

@Service
public class KakaoService {
    //...

    //Authorization코드, 즉 인가코드를 받기위한 최초요청
    public String getKakaoLogin() {
        return KAKAO_AUTH_URI + "/oauth/authorize"
                + "?client_id=" + KAKAO_CLIENT_ID
                + "&redirect_uri=" + KAKAO_REDIRECT_URL
                + "&response_type=code";
    }
}

 

request url을 위와같이 만들어서 return 해주었다.

 

컨트롤러를 하나 작성하고 서비스를 적어준다

package com.project.metakakao.controller;

import com.project.metakakao.service.KakaoService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@RequiredArgsConstructor
@Controller
public class HomeController {
    private final KakaoService kakaoService;
    @RequestMapping(value="/", method= RequestMethod.GET)
    public String login(Model model) {
        model.addAttribute("kakaoUrl", kakaoService.getKakaoLogin());

        //getKakaoLogin함수 실행, authorizatio코드 요청
        return "main";//루트 url 에서 main.jsp 리턴
    }
}

 

View로 jsp파일을 사용했다. 컨트롤러는 자유형식이므로 FE를 따로 개발해도 무관하다. 중요한건 kakaoService의 getKakaoLogin()메서드이다. getKakaoLogin 함수를 호출해 리턴받은 url을 model.addAttribute 메서드로 jsp에 넘겨주고 main.jsp로 리디렉트 한다.

 

<a href="${kakaoUrl}" class="login-button" id="kakaoLoginButton_Kr">

 

jsp 내부에는 위와같은 코드를 사용해 버튼을 클릭하면 해당 url로 연결되게하였다.

 

이정도로 작성하고 서버를 키고 카카오로그인을 시도해보자.

로그인 버튼이 존재하는 jsp 파일
해당 url로 요청을 보내면 로그인 페이지가 일단 뜬다.

로그인에 성공하면 페이지가 

http://localhost:8080/kakao/redirect?code=abcdefgFzW9AdJeP2wr1Cv0T8KPXOaAAABjMnM3xj7Ewsnpgvabc

 

위와 같은 url로 리디렉트 될것이다. 이때 code 이하의 부분이 Auth 코드이다. 

 

 

 

 

4. Auth코드 받기(토큰받기)


http://localhost:8080/kakao/redirect

?code=abcdefgFzW9AdJeP2wr1Cv0T8KPXOaAAABjMnM3xj7Ewsnpgvabc

 

카카오 로그인에 성공하면 리디렉트된 url에서 code를 빼서 엑세스토큰을 받는 요청을 해야한다.

당연히 이를위해 Controller에서 kakao/redirect 로 오는 요청을 처리하는 코드를 작성해주어야한다.

@Controller
@RequiredArgsConstructor
@RequestMapping("kakao")
public class KakaoController {
    //로그인 페이지가 뜨고, 로그인을 하면 kakao/redirect와 &code=xxx 라는 파라미터를 보내줌,
    private final KakaoService kakaoService;
    //다시 서비스를 호출하고 path가 /redirect인넘을 만나면 컨트롤러 실행
    @GetMapping("/redirect")
    public String callback(HttpServletRequest request) throws Exception {
        //HTTP 통신을 하기에 redirect uri value도 http로 설정,
        System.out.println("auth code: "+ request.getParameter("code"));//authorization 코드 뜬거 한번 출력
        Member member = kakaoService.getKakaoInfo(request.getParameter("code"), request);
        return "redirect:/member/" + member.getId();
        //메시지 엔티티는 사용하길래 해봄
    }
}

kakaoController를 정의해주고. 받아온 AuthCode를 서버에 띄워본다. (request.getParameter() 메서드 사용)

 

잘뜬다면, 이제 본격적으로 AccessToken요청을 해보자. 해당 컨트롤러에서는 kakaoService.getKakaoInfo라는 함수를 구현해서 사용하였다. 구현한 함수를 보기전에 문서를 참고하고 요청을 어떻게 보내야하는지 보자.

curl -v -X POST "https://kauth.kakao.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id=${REST_API_KEY}" \
--data-urlencode "redirect_uri=${REDIRECT_URI}" \
-d "code=${AUTHORIZE_CODE}"

 

터미널 명령어인 curl을 통해 요청을 보내고있고 해당 문법은 중요하지 않다. 어떤정보를 담는지 살펴보면

POST요청이고, 헤더에 Content-Type을 담는다.

이후 grant_type, client_id, redirect_uri, code를 각각 담아준다.

code에는 우리가 받아온 Auth코드를 담는것을 알 수 있다.

이정도만 알고 코드를 보자

@Service
public class KakaoService {

//...

    public Member getKakaoInfo(String code, HttpServletRequest request) throws Exception { // cf) alt + f7: 사용된 곳 보기
        //얘는 KakaoController의 호출로 넘어옴, KakaoDTO class의 함수
        if (code == null) throw new Exception("Failed get authorization code");
        //Authorization 코드를 받지 못할 경우 예외 리턴
        //추후에 받을 AccessToken과 Refresh토큰 선언
        String accessToken = "";
        String refreshToken = "";

        try {
            //Http헤더 객체 선언
            HttpHeaders headers = new HttpHeaders();
            //아래의 양식에 따라 전체 URL만드는 일련의 과정
            //AccessToken, RefreshToken 요청
            //curl -v -X POST "https://kauth.kakao.com/oauth/token" \
            // -H "Content-Type: application/x-www-form-urlencoded" \
            // -d "grant_type=authorization_code" \
            // -d "client_id=${REST_API_KEY}" \
            // --data-urlencode "redirect_uri=${REDIRECT_URI}" \
            // -d "code=${AUTHORIZE_CODE}"

            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("redirect_uri", KAKAO_REDIRECT_URL);
            params.add("code", code);

            RestTemplate restTemplate = new RestTemplate();
            //RestTemplate 클래스는, Java에서 RESTful 서비스를 소비하는 데 사용되는 클래스,
            //GET, POST, PUT, DELETE 등의 HTTP 메소드를 사용하여 요청을 보냄
            HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);
            System.out.println("httpEntity: " + httpEntity);
            //위에서 삽입했던 headers와 params를 가지고 Http Entity를 맹글어줌(URL 탄생)

            //ResponseEntity를 통해 restTemplate객체로 요청했을때 응답을 받아옴,
            ResponseEntity<String> response = restTemplate.exchange(
                    KAKAO_AUTH_URI + "/oauth/token",
                    HttpMethod.POST,    //POST요청
                    httpEntity,         //위에서만든 httpEntity객체
                    String.class        //restTemplate.exchange() 메서드를 호출할 때, 응답 본문을 String 타입의 객체로 변환
            );

            JSONParser jsonParser = new JSONParser();   //Json파서 선언
            JSONObject jsonObj = (JSONObject) jsonParser.parse(response.getBody());
            //JsonObj 선언 String객체로 변환한 response를 다시 JsonData로 변환

            accessToken = (String) jsonObj.get("access_token");    //access_token획득
            refreshToken = (String) jsonObj.get("refresh_token");   //refresh_token획득
            System.out.println("access_token: " + accessToken);      //출력함 해보고
            System.out.println("refresh_token: " + refreshToken);

            HttpSession session = request.getSession();
            session.setAttribute("accessToken", accessToken);
            //session.setAttribute("refreshToken", refreshToken);
            //리프토큰은 세션에 저장X

        } catch (Exception e) {
            throw new Exception("API call failed");
        }
        return getUserInfoWithToken(accessToken, refreshToken);   //리턴해서 이제 accessToken을 파라미터로 아래 함수 호출
    }

 

우선 해당 프로젝트에서는 MemberEntity를 만들어 유저클래스를 정의해줬다. 그래서 반환값이 MemberEntity이다.

주요코드를 살펴보자

 

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("redirect_uri", KAKAO_REDIRECT_URL);
params.add("code", code);

HttpHeaders 객체와 MultiValueMap을 만들고, 헤더와 파라미터를 추가해주었다.

 

RestTemplate restTemplate = new RestTemplate();
//RestTemplate 클래스는, Java에서 RESTful 서비스를 소비하는 데 사용되는 클래스,
//GET, POST, PUT, DELETE 등의 HTTP 메소드를 사용하여 요청을 보냄
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);
System.out.println("httpEntity: " + httpEntity);
//위에서 삽입했던 headers와 params를 가지고 Http Entity를 맹글어줌(URL 탄생)

 

이후 RestTemplate 객체를 만들어 HTTP 메소드를 사용해 요청을 보낼 준비를 하였다.

이후 httpEntity객체를 만들어, 추가한 헤더와 파라미터를 묶어서 하나의 httpEntity로 선언해주었다. 자세한 내용은 주석을 보자.

 

ResponseEntity<String> response = restTemplate.exchange(
        KAKAO_AUTH_URI + "/oauth/token",
        HttpMethod.POST,    //POST요청
        httpEntity,         //위에서만든 httpEntity객체
        String.class        //restTemplate.exchange() 메서드를 호출할 때, 응답 본문을 String 타입의 객체로 변환
);

 

자 이제, exchange()메서드를 통해 양식대로 요청을 보낸다. 

 

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
    "token_type":"bearer",
    "access_token":"${ACCESS_TOKEN}",
    "expires_in":43199,
    "refresh_token":"${REFRESH_TOKEN}",
    "refresh_token_expires_in":5184000,
    "scope":"account_email profile"
}

 

위에서 예시로 보였던 curl명령어를 통해 요청을 보내면 응답이 이렇게 온다.

 

JSONParser jsonParser = new JSONParser();   //Json파서 선언
JSONObject jsonObj = (JSONObject) jsonParser.parse(response.getBody());
//JsonObj 선언 String객체로 변환한 response를 다시 JsonData로 변환하여 값 추출

accessToken = (String) jsonObj.get("access_token");    //access_token획득
refreshToken = (String) jsonObj.get("refresh_token");   //refresh_token획득

이를 이해하고 우리는 JSONParser를 통해 응답을 분해해준다. 여기서 추가로 가져올 정보가 있다면, 가져와도 좋다.

 

 

HttpSession session = request.getSession();
session.setAttribute("accessToken", accessToken);
//session.setAttribute("refreshToken", refreshToken);
//리프토큰은 세션에 저장X

accessToken은 일반적으로 session에 저장해준다.

refreshToken은 일반적으로 DB에 저장한다.

 

 

return getUserInfoWithToken(accessToken, refreshToken); 

이후 accessToken과 refreshToken을 가지고 다른 작업을 처리해준다(사용자 정보 가져오기, 친구 정보 가져오기 등)

 

System.out을 통해 찍어보면 정상적으로 출력됨을 알수있다.

 

 

다음 글에서는 가져온 토큰을 가지고 사용자 정보를 가져오는 요청을 작성해보겠다.

Comments