카카오 Oauth 로그인은 2.0 표준규격에 따라 Access와 Refresh 두 종류의 토큰을 발급해주고 Uri를 통해 토큰 재발급도 해주기 때문에 일반 회원가입 로직을 만들지 않을 것이라면 토큰과 관련된 로직을 따로 만들지 않아도 되기 때문에 매우 간편하다.
이번 프로젝트에서는 일반 회원가입이 없어도 JWT를 공부하기 위해 따로 로직을 생성했으나 카카오 유저 정보를 받아오기 위해 카카오 로그인과 토큰이 필요했다.
토큰을 발급받기 위해서는 로그인을 통해 인가코드를 먼저 받아야 한다. 인가코드는 Front에서 보내주기 때문에 받은 인가코드를 통해 토큰을 받아오는 로직을 작성했다.
GetToken 로직
public OAuthToken getToken(String code) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", GRANT_TYPE);
params.add("redirect_uri", REDIRECT_URI);
params.add("client_id", CLIENT_ID);
params.add("code", code);
params.add("client_secret", CLIENT_SECRET);
WebClient wc = WebClient.create(TOKEN_URI);
log.info("토큰을 요청하는 중...");
// POST 방식으로 key-value 데이터 요청
OAuthToken oauthTokenRes = wc.post()
.uri(TOKEN_URI)
.body(BodyInserters.fromFormData(params))
.header("Content-type", "application/x-www-form-urlencoded;charset=utf-8")
.retrieve()
.bodyToMono(OAuthToken.class).block();
log.info("토큰 발급 완료!");
return oauthTokenRes;
}
getToken을 통해 받아온 토큰을 가질 OauthToken 객체
@Getter
@NoArgsConstructor
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class OAuthToken {
private String accessToken;
private String tokenType;
private String refreshToken;
private Integer expiresIn;
private Integer refreshTokenExpiresIn;
private String error;
private String errorDescription;
}
웹으로 API를 호출하기 위해 WebClient를 사용했다. RestTemplate는 현재 deprecated 상태이며, Webclient가 권장된다고 하여 Webclient를 사용했다.
이유를 찾아보니 RestTemplate는 Blocking I/O를 사용하여 한 번에 하나의 작업만 처리가 가능한데 WebClient는 비동기 및 NonBlocking I/O를 지원하며 람다,스트림 프로그래밍과 에러 핸들링에 더욱 장점이 있다고 한다.
grant_type은
authorization_code
으로 항상 고정 값이며, 그 외 redirect_uri, client_id, client_secret은 카카오 Developer에 저장한 내용으로 지정해야한다.
Front에서 보내준 인가코드로 토큰을 요청하자 토큰을 받아오지 못하고 400에러가 발생했다. Front에 계속 요청하며 수정하다가 계속 코드를 요청하기 미안해서 직접 인가코드를 받아와서 테스트해보았다.
포스트 맨을 이용하면 간편하게 요청 Uri를 만들 수 있다.
client_id와 redirect_id를 설정한 값으로 지정해주고 해당 uri를 브라우저 주소창에 입력하고 Kakao로그인을 하면, 다음과 같이 코드를 얻을 수 있다.
code=
뒷 부분을 복사하여 토큰을 요청했다.
❓ 받아온 인가코드로는 안되는데, 내가 보낸 코드로는 바로 토큰이 불러와졌다.
알고보니 Front쪽에서 사용하는 redirect_uri가 따로 있었다.
인가코드는 redirect_uri A로 하고, 토큰은 redirect_uri B로 해서 생기는 문제였다.인가코드와 토큰 발급은 모두 일련의 과정이기에 같은 redirect_uri를 사용해야했다.
KakaoProfile객체로 받아올 정보 요청
public KakaoProfile getMemberInfo(String accessToken) {
KakaoProfile profile = webClient.mutate()
.baseUrl(USER_INFO_URI)
.build()
.get()
.uri("/v2/user/me")
.headers(h -> h.setBearerAuth(accessToken))
.retrieve()
.bodyToMono(KakaoProfile.class)
.block();
return profile;
}
baseUri는
https://kapi.kakao.com
, 추가 uri는/v2/user/me
로 고정이다.
headers를 사용해 BearerAuth로 받아온 accessToken을 넣어준다.
KakaoProfile.class
@Getter
public class KakaoProfile {
private Long id;
private String connected_at;
private Properties properties;
private KakaoAccount kakao_account;
@Getter
public static class Properties {
private String nickname;
private String profile_image;
private String thumbnail_image;
}
@Getter
public static class KakaoAccount {
private Boolean profile_needs_agreement;
private Profile profile;
private Boolean has_email;
private Boolean email_needs_agreement;
private Boolean is_email_valid;
private Boolean is_email_verified;
private String email;
private Boolean has_age_range;
private Boolean age_range_needs_agreement;
private Boolean has_birthday;
private Boolean birthday_needs_agreement;
private Boolean has_gender;
private Boolean gender_needs_agreement;
@Getter
public static class Profile {
private String nickname;
private String thumbnail_image_url;
private String profile_image_url;
}
}
}
public OAuthSignInResponse redirect(String code) {
OAuthToken oAuthToken = getToken(code);
KakaoProfile kakaoProfile = getMemberInfo(oAuthToken.getAccessToken());
Long id = kakaoProfile.getId();
String nickname = kakaoProfile.getKakao_account().getProfile().getNickname();
Optional<Member> member = memberRepository.findByMemberId(id);
String email = kakaoProfile.getKakao_account().getEmail();
JwtToken jwtToken = jwtProvider.createToken(id, email);
return OAuthSignInResponse oAuthSignInResponse = OAuthSignInResponse.builder()
.id(id)
.nickname(nickname)
.email(email)
.accessToken(jwtToken.getAccessToken())
.refreshToken(jwtToken.getRefreshToken())
.refreshTokenExpiration(jwtToken.getRefreshTokenExpirationTime())
.build();
Front로 보내줄 ResponseDto
@Getter
public class OAuthSignInResponse {
private Long id;
private String nickname;
private String email;
private String accessToken;
private String refreshToken;
private Date refreshTokenExpiration;
@Builder
public OAuthSignInResponse(Long id, String nickname, String email, String accessToken, String refreshToken, Date refreshTokenExpiration) {
this.id = id;
this.nickname = nickname;
this.email = email;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.refreshTokenExpiration = refreshTokenExpiration;
}
인가코드(code)로 getToken을 이용해 토큰을 받아오고 받아온 토큰에서 accessToken으로
해당 유저의 KakaoProfile정보를 요청했다. 받아오는 유저 정보는 kakaoDevelopers에서 설정할 수 있다.
kakao로 받은 토큰은 최초 회원가입 시에만 사용하여 유저 정보를 가져오고, 그 이후에는 내용을 저장하여 로그인 시 이미 등록된 회원이면 내가 직접 구현한 jwtProvider을 통해 만든 Token을 사용했다.kakao에서 토큰 재발급까지 모두 API로 제공해주지만 프로젝트 간에는 공부를 위해 직접 JWT를 구현해서 사용하였다.
공부는 잘 되었지만 뭔가 이상했다. Oauth랑 JWT 둘 중 하나만 사용하면 되는데 굳이굳이 두개를 섞어 쓴 느낌? 공부하는 입장에서는 짬짜면으로 둘 다 즐긴거지만 프로젝트 진행측면에서는 짬뽕과 짜장면을 비벼 먹은 느낌이다. 둘 중 하나만 썼다면 하루, 이틀은 소요가 줄었을 것 같다고 느꼈다.
OauthController.class
@PostMapping("")
@Operation(summary = "카카오 로그인", description = "카카오 로그인 or 회원가입")
public ResponseEntity<OAuthSignInResponse> redirect(@RequestParam String code) {
OAuthSignInResponse oAuthSignInResponse = oauthService.redirect(code);
return ResponseEntity.ok(oAuthSignInResponse);
controller를 추가하여 기본 uri로 코드를 보내면 Response로 유저 정보와 토큰을 넘긴다. 매우 잘 넘어간다.
🆘생각지 못하게 발생한 에러🆘
Redirect를 바꾸고 나서도 토큰을 요청할 때 문제가 발생하였는데,
받은 인가코드로 토큰을 두 번 요청해서였다.
로그엔 400 Bad Request가 계속 뜨니 원인을 찾는데 좀 걸렸다.
kakao 인가코드는 유효기간 1분에 발급받은 인가코드로 요청 시, 결과가 성공이든 실패든 단 한번만 쓸 수 있다고 한다. 유효기간도 짧고 1회성이기 때문에 같은 코드로 테스트 중이거나 한 번에 2번씩 요청이 가게 되면 토큰을 발급받고도 한번 더 요청을 하는 것이기에 에러가 발생할 수 있다.
도중에 두 번 요청되는 이유를 알지 못해 kakaodevTalk에 질문을 했는데 응답시간이 대부분 10~20분 이내로 매우 빨랐다. 카카오는 매우 친절하다. 정말 이유를 알 수 없다면 devTalk을 활용하자.