Skip to content

Commit

Permalink
Merge pull request #34 from DKU-Dgaja/feat(#29)-Kakao-oauth-login
Browse files Browse the repository at this point in the history
  • Loading branch information
rndudals authored Jan 19, 2024
2 parents d440bba + aebd5a8 commit 199c59c
Show file tree
Hide file tree
Showing 11 changed files with 574 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
import com.example.backend.auth.api.controller.auth.response.AuthLoginPageResponse;
import com.example.backend.auth.api.service.oauth.adapter.OAuthAdapter;
import com.example.backend.auth.api.service.oauth.adapter.github.GithubAdapter;
import com.example.backend.auth.api.service.oauth.adapter.google.GoogleAdapter;

import com.example.backend.auth.api.service.oauth.adapter.kakao.KakaoAdapter;
import com.example.backend.auth.api.service.oauth.builder.OAuthURLBuilder;
import com.example.backend.auth.api.service.oauth.builder.github.GithubURLBuilder;
import com.example.backend.auth.api.service.oauth.builder.kakao.KakaoURLBuilder;

import com.example.backend.auth.api.service.oauth.adapter.google.GoogleAdapter;
import com.example.backend.auth.api.service.oauth.builder.google.GoogleURLBuilder;

import com.example.backend.auth.api.service.oauth.response.OAuthResponse;
import com.example.backend.domain.define.user.constant.UserPlatformType;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -19,6 +24,7 @@
import java.util.stream.Collectors;

import static com.example.backend.domain.define.user.constant.UserPlatformType.GITHUB;
import static com.example.backend.domain.define.user.constant.UserPlatformType.KAKAO;
import static com.example.backend.domain.define.user.constant.UserPlatformType.GOOGLE;

@Slf4j
Expand All @@ -28,18 +34,28 @@ public class OAuthService {
private Map<UserPlatformType, OAuthFactory> adapterMap;

// 플랫폼별 Adapter, URLBuilder 등록
public OAuthService(GithubAdapter githubAdapter, GithubURLBuilder githubURLBuilder, GoogleAdapter googleAdapter, GoogleURLBuilder googleURLBuilder) {


public OAuthService(GithubAdapter githubAdapter, GithubURLBuilder githubURLBuilder, GoogleAdapter googleAdapter, GoogleURLBuilder googleURLBuilder , KakaoAdapter kakaoAdapter, KakaoURLBuilder kakaoURLBuilder) {
this.adapterMap = new HashMap<>() {{
// 깃허브 플랫폼 추가
put(GITHUB, OAuthFactory.builder()
.oAuthAdapter(githubAdapter)
.oAuthURLBuilder(githubURLBuilder)
.build());

// 카카오 플랫폼 추가
put(KAKAO, OAuthFactory.builder()
.oAuthAdapter(kakaoAdapter)
.oAuthURLBuilder(kakaoURLBuilder)
.build());

// 구글 플랫폼 추가
put(GOOGLE, OAuthFactory.builder()
.oAuthAdapter(googleAdapter)
.oAuthURLBuilder(googleURLBuilder)
.build());

}};
}
// OAuth 2.0 로그인 페이지 생성
Expand Down Expand Up @@ -77,7 +93,6 @@ public OAuthResponse login(UserPlatformType platformType, String code, String st

// Access Token 획득
String accessToken = adapter.getToken(tokenUrl);

// 사용자 프로필 조회
OAuthResponse userInfo = adapter.getProfile(accessToken);
log.info(">>>> [ {} Login Success ] <<<<", platformType);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.example.backend.auth.api.service.oauth.adapter.kakao;

import com.example.backend.auth.api.service.oauth.adapter.OAuthAdapter;
import com.example.backend.auth.api.service.oauth.response.OAuthResponse;
import com.example.backend.common.exception.ExceptionMessage;
import com.example.backend.common.exception.oauth.OAuthException;
import com.example.backend.external.clients.oauth.kakao.KakaoProfileClients;
import com.example.backend.external.clients.oauth.kakao.KakaoTokenClients;
import com.example.backend.external.clients.oauth.kakao.response.KakaoProfileResponse;
import com.example.backend.external.clients.oauth.kakao.response.KakaoTokenResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.net.URI;

import static com.example.backend.domain.define.user.constant.UserPlatformType.KAKAO;

@Slf4j
@Component
@RequiredArgsConstructor
public class KakaoAdapter implements OAuthAdapter {
private final KakaoTokenClients kakaoTokenClients;
private final KakaoProfileClients kakaoProfileClients;

@Override
public String getToken(String tokenURL) {
try {
KakaoTokenResponse token = kakaoTokenClients.getToken(URI.create(tokenURL));

// 받아온 token이 null일 경우 예외 발생
if (token.getAccess_token() == null) {
throw new OAuthException(ExceptionMessage.OAUTH_INVALID_TOKEN_URL);
}

return token.getAccess_token();
} catch (RuntimeException e) {
log.error(">>>> [ Kakao Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_TOKEN_URL.getText());
throw new OAuthException(ExceptionMessage.OAUTH_INVALID_TOKEN_URL);
}
}

@Override
public OAuthResponse getProfile(String accessToken) {
try {
KakaoProfileResponse profile = kakaoProfileClients.getProfile("Bearer " + accessToken);

return OAuthResponse.builder()
.platformId(profile.getId().toString())
.platformType(KAKAO)
.name(profile.getProperties().getNickname())
.profileImageUrl(profile.getProperties().getProfile_image())
.build();
} catch (RuntimeException e) {
log.error(">>>> [ Kakao Oauth 인증 에러 발생: {}", ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN.getText());
throw new OAuthException(ExceptionMessage.OAUTH_INVALID_ACCESS_TOKEN);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.example.backend.auth.api.service.oauth.builder.kakao;

import com.example.backend.auth.api.service.oauth.builder.OAuthURLBuilder;
import com.example.backend.auth.config.oauth.OAuthProperties;
import com.example.backend.common.exception.ExceptionMessage;
import com.example.backend.common.exception.oauth.OAuthException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class KakaoURLBuilder implements OAuthURLBuilder {
private static final String PLATFORM = "kakao";
private final String authorizationUri;
private final String clientId;
private final String redirectUri;
private final String tokenUri;
private final String clientSecret;
private final String profileUri;


// 속성에서 읽어온 객체를 주입
public KakaoURLBuilder(OAuthProperties oAuthProperties) {
try {
// 플랫폼(kakao)의 client, provider Map 획득
OAuthProperties.Client kakaoClient = oAuthProperties.getClient().get(PLATFORM);
OAuthProperties.Provider kakaoProvider = oAuthProperties.getProvider().get(PLATFORM);

this.authorizationUri = kakaoProvider.authorizationUri();
this.clientId = kakaoClient.clientId();
this.redirectUri = kakaoClient.redirectUri();
this.tokenUri = kakaoProvider.tokenUri();
this.clientSecret = kakaoClient.clientSecret();
this.profileUri = kakaoProvider.profileUri();

} catch (NullPointerException e) {
log.error(">>>> OAuthProperties NullPointerException 발생: {}", ExceptionMessage.OAUTH_CONFIG_NULL);
throw new OAuthException(ExceptionMessage.OAUTH_CONFIG_NULL);
}
}
// "https://kauth.kakao.com/oauth/authorize?..."
@Override
public String authorize(String state) {
return authorizationUri
+ "?response_type=code" // OAuth 인증 코드 그랜트 유형: code로 고정
+ "&client_id=" + clientId // 클라이언트 ID
+ "&redirect_uri=" + redirectUri // 리다이렉트 URI
+ "&state=" + state // CSRF 방지
+ "&scope=openid"; // 리소스 접근 범위: openid로 고정
}

// "https://kauth.kakao.com/oauth/token?..."
@Override
public String token(String code, String state) {
return tokenUri
+ "?grant_type=authorization_code" // OAuth 인증 코드 그랜트 유형: code로 고정
+ "&client_id=" + clientId // 클라이언트 ID
+ "&client_secret=" + clientSecret // 클라이언트 Secret
+ "&redirect_uri=" + redirectUri // 리다이렉트 URI
+ "&code=" + code; // authorize() 요청으로 얻은 인가 코드

}
// "https://kapi.kakao.com/v2/user/me"
@Override
public String profile() { return profileUri; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.backend.external.clients.oauth.kakao;

import com.example.backend.external.annotation.ExternalClients;
import com.example.backend.external.clients.oauth.kakao.response.KakaoProfileResponse;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.service.annotation.GetExchange;

/*
사용자 정보를 얻기 위해 설정한 profile 요청 URI로 GET 요청을 보낸다.
*/
@ExternalClients(baseUrl = "https://kapi.kakao.com/v2/user/me")
public interface KakaoProfileClients {

@GetExchange
public KakaoProfileResponse getProfile(@RequestHeader(value = "Authorization") String header);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.backend.external.clients.oauth.kakao;


import com.example.backend.external.annotation.ExternalClients;
import com.example.backend.external.clients.oauth.kakao.response.KakaoTokenResponse;
import org.springframework.http.MediaType;
import org.springframework.web.service.annotation.PostExchange;

import java.net.URI;

/*
AccessToken을 얻기 위해 설정한 token URI로 POST 요청을 보낸다.
*/
@ExternalClients(baseUrl = "oauth2.provider.kakao.token-uri")
public interface KakaoTokenClients {

// 요청의 content Type: FORM_URLENCODED 형식 ex) key1=value1&key2=value2
@PostExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public KakaoTokenResponse getToken(URI uri);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.backend.external.clients.oauth.kakao.response;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class KakaoProfileResponse {
private Long id;
private String name;

private Properties properties;

public KakaoProfileResponse(Long id, String name, Properties properties) {
this.id = id;
this.name = name;
this.properties = properties;
}

@Getter
@NoArgsConstructor
public static class Properties {
private String nickname;
private String profile_image;
private String thumbnail_image;

public Properties(String nickname, String profile_image, String thumbnail_image) {
this.nickname = nickname;
this.profile_image = profile_image;
this.thumbnail_image = thumbnail_image;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.backend.external.clients.oauth.kakao.response;


import lombok.Getter;
import lombok.NoArgsConstructor;

/*
Kakao에 Access Token을 요청 후 반환받을 DTO
*/
@Getter
@NoArgsConstructor
public class KakaoTokenResponse {
private String access_token;

public KakaoTokenResponse(String access_token) {
this.access_token = access_token;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import com.example.backend.auth.api.controller.auth.response.AuthLoginPageResponse;
import com.example.backend.auth.api.service.jwt.JwtService;
import com.example.backend.auth.api.service.oauth.adapter.github.GithubAdapter;
import com.example.backend.auth.api.service.oauth.adapter.kakao.KakaoAdapter;
import com.example.backend.auth.api.service.oauth.builder.github.GithubURLBuilder;
import com.example.backend.auth.api.service.oauth.builder.kakao.KakaoURLBuilder;
import com.example.backend.auth.api.service.oauth.adapter.google.GoogleAdapter;
import com.example.backend.auth.api.service.oauth.builder.google.GoogleURLBuilder;
import com.example.backend.auth.api.service.oauth.response.OAuthResponse;
Expand All @@ -20,6 +22,7 @@
import java.util.List;

import static com.example.backend.domain.define.user.constant.UserPlatformType.GITHUB;
import static com.example.backend.domain.define.user.constant.UserPlatformType.KAKAO;
import static com.example.backend.domain.define.user.constant.UserPlatformType.GOOGLE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
Expand All @@ -37,6 +40,11 @@ class OAuthServiceTest extends TestConfig {
private GithubAdapter githubAdapter;

@Autowired

private KakaoURLBuilder kakaoUrlBuilder;
@MockBean
private KakaoAdapter kakaoAdapter;

private GoogleURLBuilder googleurlBuilder;

@MockBean
Expand All @@ -51,6 +59,10 @@ void allUrlBuilderSuccess() {
// when
List<AuthLoginPageResponse> loginPages = oAuthService.loginPage(state);
String authorizeURL = urlBuilder.authorize(state);

String authorizeURLkakao = kakaoUrlBuilder.authorize(state);
// then

String authorizeURLgoogle = googleurlBuilder.authorize(state);

// then
Expand All @@ -62,6 +74,12 @@ void allUrlBuilderSuccess() {
boolean containsGithub = loginPages.stream()
.anyMatch(page -> page.getPlatformType().equals(GITHUB) &&
page.getUrl().equals(authorizeURL));
boolean containsKakao = loginPages.stream()
.anyMatch(page -> page.getPlatformType().equals(KAKAO) &&
page.getUrl().equals(authorizeURLkakao));

assertThat(containsGithub).isTrue();
assertThat(containsKakao).isTrue();
boolean containsGoogle = loginPages.stream()
.anyMatch(page -> page.getPlatformType().equals(GOOGLE) &&
page.getUrl().equals(authorizeURLgoogle));
Expand Down Expand Up @@ -94,7 +112,35 @@ void githubLoginSuccess() {
}

@Test
@DisplayName("깃허브 로그인에 실하면 OAuthException 예외가 발생한다.")
@DisplayName("카카오 로그인에 성공하면 OAuthResponse 객체를 반환한다.")
void kakaoLoginSuccess() {
// given
String code = "valid-code";
String state = "valid-state";

OAuthResponse response = OAuthResponse.builder()
.platformId("1")
.platformType(KAKAO)
.email("이메일 없음")
.name("구영민")
.profileImageUrl("http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_640x640.jpg")
.build();

// when
// when 사용시 Mockito 패키지 사용
when(kakaoAdapter.getToken(any(String.class))).thenReturn("access-token");
when(kakaoAdapter.getProfile(any(String.class))).thenReturn(response);
OAuthResponse profile = oAuthService.login(KAKAO, code, state);

// then
assertThat(profile)
.extracting("platformId", "platformType", "email", "name", "profileImageUrl")
.contains("1", KAKAO, "이메일 없음", "구영민", "http://k.kakaocdn.net/dn/1G9kp/btsAot8liOn/8CWudi3uy07rvFNUkk3ER0/img_640x640.jpg");


}
@Test
@DisplayName("깃허브 로그인에 실패하면 OAuthException 예외가 발생한다.")
void githubLoginFail() {
// given
String code = "invalid-code";
Expand Down Expand Up @@ -148,4 +194,18 @@ void googleLoginFail() {
() -> oAuthService.login(GOOGLE, code, state));

}

@Test
@DisplayName("카카오 로그인에 실패하면 OAuthException 예외가 발생한다.")
void kakaoLoginFail() {
// given
String code = "invalid-code";
String state = "invalid-state";

// when
when(kakaoAdapter.getToken(any(String.class))).thenThrow(OAuthException.class);
assertThrows(OAuthException.class,
() -> oAuthService.login(KAKAO, code, state));

}
}
Loading

0 comments on commit 199c59c

Please sign in to comment.