이번에 11월 런칭 목표인 새로운 서비스를 개발하게 됐다.
이메일 로그인은 MVP에서 제외된 기능이고, 우선은 소셜 로그인부터 구현해야 했다.
그동안 RestTemplate밖에 경험이 없었기에, 그 유명한 FeignClient를 사용해보기로 했다.
이전에 RestTemplate을 사용했을 때보다 훨씬 가독성이 좋아졌고,
인터페이스를 정의하고 어노테이션을 붙이는 것만으로 HTTP 클라이언트를 설정할 수 있어서 코드가 간결해졌다..!
의존성 추가 - build.gradle
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.2'
당연하겠지만, 버전을 명시하지 않아도 동작한다.
우리는 다른 의존성과의 호환성 문제가 발생할 가능성을 생각해 버전을 명시해줬다.
@EnableFeignClients 추가 - FeignClientConfig
@Configuration
@EnableFeignClients(basePackages = {"패키지 지정 가능"})
public class FeignClientConfig {
}
FeignClient는 인터페이스 형식으로 정의하여 REST API 요청한다.
사용 예시 - KakaoAuthClient
@FeignClient(value = "kakao-auth", url = "${api.kakao.kauth}") // url은 프로퍼티로 지정함
public interface KakaoAuthClient {
@PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
KakaoToken generateOAuthToken(@RequestParam(name = "grant_type") String grantType,
@RequestParam(name = "client_id") String clientId,
@RequestParam(name = "redirect_uri") String redirectUri,
@RequestParam(name = "code") String code,
@RequestParam(name = "client_secret") String clientSecret);
}
사용 예시 - KakaoApiClient
@FeignClient(value = "kakao-api", url = "${api.kakao.kapi}")
public interface KakaoApiClient {
@GetMapping("/v2/user/me")
KakaoUser getUserInfo(@RequestHeader(HttpHeaders.AUTHORIZATION) String accessToken);
}
구글 로그인 구현
Controller
@Operation(summary = "구글 로그인")
@PostMapping("/login/google")
public ResponseEntity<CommonResponse<LoginResponse>> googleLogin(@Valid @RequestBody GoogleLoginRequest googleLoginRequest) {
return CommonResponse.success(SuccessCode.SUCCESS, memberService.login(googleLoginRequest.toDto()));
}
GoogleLoginRequest
public record GoogleLoginRequest(
@NotBlank
String code,
@JsonProperty("redirectUri")
String redirectUri
) {
public OAuthLoginInfo toDto() {
Map<String, String> propertyMap = Map.of(
AuthConstants.CODE, code,
AuthConstants.REDIRECT_URI, redirectUri
);
return new OAuthLoginInfo(Provider.GOOGLE, propertyMap);
}
}
인가코드와 리다이렉트 Uri를 받아서, 맵으로 지정해준다.
Provider에는 GOOGLE, KAKAO와 같은 가입 경로들이 열거형으로 들어있다.
OAuthLoginInfo
public record OAuthLoginInfo(
Provider provider,
Map<String, String> propertyMap
) {
}
Service
@Transactional
public LoginResponse login(OAuthLoginInfo authLoginInfo) {
// 인가 코드를 이용해 액세스 토큰 발급 요청
String accessToken = providerFactory.getAccessToken(
authLoginInfo.provider(),
authLoginInfo.propertyMap().get(AuthConstants.REDIRECT_URI),
authLoginInfo.propertyMap().get(AuthConstants.CODE)
);
// 액세스 토큰을 이용해 사용자 정보 가져오기
OAuthUser oAuthUser = providerFactory.getOAuthUser(authLoginInfo.provider(), accessToken);
// 회원가입 or 로그인 로직
// 액세스 토큰 발급
String token = jwtTokenProvider.createAccessToken(oAuthUser.email());
return new LoginResponse(new Token(token, jwtTokenProvider.getExpirationByToken(token)));
}
순서는 위와 같이,
1. 프론트에게 받은 인가 코드로 토큰을 발급 받는다.
2. 받은 토큰으로, 해당 사용자 정보를 가져온다.
3. 해당 사용자가 우리 db에 존재하는 회원인지 아닌 지에 따라 로직이 바뀐다. -> 회원가입 or 로그인
이는 서비스마다 구현 방식이 다를 것으로 예상된다.
4. 그 후에, 프론트에게 내어 줄 액세스 토큰을 발급하고 전달한다.
OAuthProviderFactory
public String getAccessToken(Provider provider, String redirectUri, String code) {
return getOAuthToken(provider, redirectUri, code).accessToken();
}
private OAuthToken getOAuthToken(Provider provider, String redirectUri, String code) {
return getOAuthProvider(provider).getOAuthToken(redirectUri, code);
}
private OAuthProvider getOAuthProvider(Provider provider) {
OAuthProvider oAuthProvider = OAuthProviderMap.get(provider);
if (Objects.isNull(oAuthProvider)) {
throw new InvalidParamException(ErrorCode.NOT_FOUND_OAUTH_PROVIDER);
}
return oAuthProvider;
}
이해를 위해 일부 핵심 코드만 갖고 왔지만,
다른 코드는 Map을 init해주는 코드 말고는 없다..!
--> 여기서는 해당 Provider로 값을 찾아, Google이라면 GoogleProvider의 구현을 따르기로 한다.
OAuthProvider
public interface OAuthProvider {
OAuthToken getOAuthToken(String redirectUri, String code);
OAuthUser getOAuthUser(String accessToken);
}
이 OAuthProvider는 인터페이스로 작성하여, 각 소셜마다 다르게 구현할 수 있게 했다.
-아래에 GoogleToken 구현으로 볼 수 있다.
GoogleProvider
@Override
public OAuthToken getOAuthToken(String redirectUri, String code) {
String decodedCode = URLDecoder.decode(code, StandardCharsets.UTF_8); // 구글 oauth 서버로부터 받은 인가코드는 디코딩 해줘야 함
return googleAuthClient.generateOAuthToken(
decodedCode,
clientId,
clientSecret,
redirectUri,
grantType
);
}
위는 토큰을 받기 위한 구조로 보면 된다.
* api 문서에 있는 그대로 순서를 꼭 지켜야 하고, 구글은 카카오와 달리 인가 코드를 디코딩하여 보내줘야 한다..!
OAuthToken
public interface OAuthToken {
String tokenType();
String accessToken();
int expiresIn();
String refreshToken();
String scope();
int refreshTokenExpiresIn();
}
각 소셜마다 값이 다르게 내려올 수 있으므로, 위처럼 인터페이스로 정의한다.
GoogleAuthClient
@FeignClient(value = "google-auth", url = "${api.google.gauth}")
public interface GoogleAuthClient {
@PostMapping(value = "/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
GoogleToken generateOAuthToken(@RequestParam(name = "code") String code,
@RequestParam(name = "client_id") String clientId,
@RequestParam(name = "client_secret") String clientSecret,
@RequestParam(name = "redirect_uri") String redirectUri,
@RequestParam(name = "grant_type") String grantType);
}
위와 같이 서버에 요청을 하면,
GoogleToken
public record GoogleToken(
@JsonProperty("token_type") String tokenType,
@JsonProperty("access_token") String accessToken,
@JsonProperty("expires_in") int expiresIn,
@JsonProperty("refresh_token") String refreshToken,
@JsonProperty("scope") String scope,
@JsonProperty("id_token") String idToken
) implements OAuthToken {
}
이렇게 따로 설정한 GoogleToken에 값이 전해진다.
이렇게 인가코드로 토큰을 받는 1번 과정은 끝이 났다.
다음으로 사용자 정보를 받는 2번 과정을 보자.
GoogleProvider
@Override
public OAuthUser getOAuthUser(String accessToken) {
return googleApiClient.getUserInfo(getBearerToken(accessToken));
}
위에서 봤던 코드들은 생략했다.
ProviderFactory에서 해당 소셜 구현체로 getOAuthUser를 호출한다.
GoogleApiClient
@FeignClient(value = "google-api", url = "${api.google.gapi}")
public interface GoogleApiClient {
@GetMapping("/userinfo/v2/me")
GoogleUser getUserInfo(@RequestHeader(HttpHeaders.AUTHORIZATION) String accessToken);
}
위와 같은 경로로 요청을 보내면, GoogleUser로 값을 받을 수 있다.
이 코드는 카카오와 같다!
GoogleUser
public record GoogleUser(
@JsonProperty("id") String id,
@JsonProperty("email") String email,
@JsonProperty("verified_email") boolean verifiedEmail,
@JsonProperty("picture") String picture
) implements OAuthUser{
}
위 4개 값은 필수 값이므로 꼭 받아주어야 에러가 나지 않는다.
이렇게 되면 사용자 정보를 받아오는 2번 과정까지 마쳤다.
이후에 토큰 발급과 같은 로직은 워낙 흔하게 포스팅되어 있으니, 넘어가겠다..!
주의사항
구글 로그인은 api 문서가 참 여기저기 흩어져 있다.
내가 참고한 문서 링크를 공유해놓겠다..!
https://cloud.google.com/identity-platform/docs/use-rest-api?hl=ko#section-refresh-token
REST API 사용 | Identity Platform 문서 | Google Cloud
의견 보내기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. REST API 사용 이 문서에서는 Identity Platform REST API를 사용하여 사용자 로그인 및 토큰 작업 등의 일
cloud.google.com
처음에는 이 문서를 보고 작업을 하다가..
url들이 어떤 블로그에도 나오지 않는 엔드포인트인 것이다........
그렇게 또 api 문서를 파 본 결과
https://developers.google.com/identity/protocols/oauth2/web-server?hl=ko#exchange-authorization-code
웹 서버 애플리케이션용 OAuth 2.0 사용 | Authorization | Google for Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 의견 보내기 웹 서버 애플리케이션용 OAuth 2.0 사용 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이
developers.google.com
위 링크를 찾았다!
그렇게 요청 url과 요청 필수 값들을 넣는 것에는 성공했지만,
갑자기 id_token이 없다는 에러가 뜬다..?
분명 위 문서에는 응답 필드에
이렇게 다섯 가지만 존재한다.
더 구글링 해 본 결과
https://developers.google.com/identity/openid-connect/openid-connect?hl=ko#offlineaccess
OpenID Connect | Authentication | Google for Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 의견 보내기 OpenID Connect 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Google의 OAuth 2.0 API는 인증과 승
developers.google.com
이 문서를 찾을 수 있었다.
이렇게 id_token을 찾을 수 있었다.
뭐가 맞는 지 이젠 나도 모르겠다...
이렇게 token을 발급받는 문서는 찾았으나,
이틀 내내 관련 레퍼런스를 최대한 뒤져봐도 사용자 정보를 받아오는 올바른 url과 응답값을 소개하는 문서를 찾을 수가 없었다..
api 문서를 찾으면 바로 추가 포스팅하도록 하겠다.
---
아무리 찾아봐도 없고, 이전에 나와 같은 요청 url을 포스팅하신 분들의 글을 찾아보니
모두 내가 봤던 문서를 보셔서 나도 해당 문서를 다시 찾아봤다.
https://cloud.google.com/identity-platform/docs/use-rest-api?hl=ko#section-get-account-info
REST API 사용 | Identity Platform 문서 | Google Cloud
의견 보내기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. REST API 사용 이 문서에서는 Identity Platform REST API를 사용하여 사용자 로그인 및 토큰 작업 등의 일
cloud.google.com
위 링크인데, 아무리 찾아도 해당 url은 없다.
그래서 내가 내린 결론인데, 아무래도 구글에서 api 문서를 업데이트하고 이전 문서는 전부 비공개로 돌린 것 같다...
응답구조를 보고 싶은데, 새로운 api 문서를 따라서 새로 만들어야겠다..
'대충 넘어가지 않는 습관을 위한 기록' 카테고리의 다른 글
ArgumentResolver 응용 기록 (0) | 2024.08.13 |
---|---|
GitHub Actions 워크플로우 이벤트 수동 전환 (0) | 2024.07.18 |
@Mapping 활용법 - boolean (0) | 2024.06.01 |
Querydsl - DTO 반환 (0) | 2024.05.02 |
JPA 기록 - 지연 로딩과 조회 성능 최적화 (1) | 2024.04.23 |