기존 게시글
에서 Front-End 에서 Back-End 에 등록되어있는 소셜로그인 링크를 a 태그로 이동하면
회원가입과 Redirect 를 Back-End 에서 진행을 했었습니다.
문제점
기능적인 문제 X
개발자의 편의성 향상 O
위 의 문제점으로 인해 아래 사진과 같이 Flow 를 변경 할 예정입니다.
Back-End 에서 Authorization code 발급부터 회원 관리까지 하던것을
Front-End 에게 code 와 Redirect Uri 정보를 받아 기존의 문제점을 극복하고자 했습니다.
현재 Baeker 에서는 Kakao 로그인만 제공합니다. 추후에 Google, naver 등 다른 기능 추가의 필요성을 느낀다면 controller 명과 @GetMapping 으로 정의함으로 유연하게 전환 하는것을 고려했습니다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/login/oauth2")
public class KakaoController {
private final KakaoService kakaoService;
@GetMapping("/kakao")
public SocialLoginResponse kakaoLogin(String code, String redirectUri) {
return kakaoService.kakaoLogin(code, redirectUri);
}
}
@Service
public class CustomOidcUserService extends AbstractOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
@Override
@Transactional
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
// Open ID Connect 인 경우 User name Attribute Key 가 sub 이기 때문에 재정의함
ClientRegistration clientRegistration = ClientRegistration
.withClientRegistration(userRequest.getClientRegistration())
.userNameAttributeName("sub")
.build();
OidcUserRequest oidcUserRequest =
new OidcUserRequest(clientRegistration, userRequest.getAccessToken(),
userRequest.getIdToken());
OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService = new OidcUserService();
OidcUser oidcUser = null;
try {
oidcUser = oidcUserService.loadUser(oidcUserRequest);
} catch (Exception e) {
e.printStackTrace();
}
ProviderUserRequest providerUserRequest = new ProviderUserRequest(clientRegistration,oidcUser);
ProviderUser providerUser = providerUser(providerUserRequest);
selfCertificate(providerUser);
super.register(providerUser, oidcUserRequest);
return new PrincipalUser(providerUser);
}
}
저희는 Oidc 프로토콜을 사용하고, 책임과 분리를 위해 OidcUserService 를 재정의 해줬습니다.
이를 사용하려면 각 Provider 에 맞는 서비스를 제공하고 위 loadUser 메서드를 활용하면 됩니다.
public SocialLoginResponse kakaoLogin(String code, String redirectUri) {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add(CONTENT_TYPE, "application/x-www-form-urlencoded;charset=utf-8");
String body = "grant_type=" + authorizationGrantType.getValue() + "&client_id=" + clientId + "&redirect_uri=" + redirectUri + "&code=" + code + "&client_secret=" + clientSecret;
HttpEntity<String> request = new HttpEntity<>(body, headers);
String response = restTemplate.postForObject(tokenUri, request, String.class);
log.info("카카오 로그인 response : {}", response);
SocialLoginResponse socialLoginResponse = null;
try {
socialLoginResponse = getOidcTokenId(response, redirectUri);
} catch (JsonProcessingException | ParseException e) {
throw new JwtCreateException(JWT_CREATE_EXCEPTION.getMessage());
}
return socialLoginResponse;
}
private SocialLoginResponse getOidcTokenId(String response, String redirectUri) throws JsonProcessingException, ParseException {
JSONObject jsonObject = Json.mapper().readValue(response, JSONObject.class);
String idToken = jsonObject.get("id_token").toString();
String oauth2TokenId = jsonObject.get("access_token").toString();
String scopes = jsonObject.get("scope").toString();
JWT parse = JWTParser.parse(idToken);
String subject = parse.getJWTClaimsSet().getSubject();
OidcIdToken oidcIdToken = OidcIdToken.withTokenValue(idToken)
.tokenValue(idToken)
.subject(subject)
.build();
OAuth2AccessToken oAuth2AccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, oauth2TokenId, null, null);
String[] scope = scopeParsing(scopes);
ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(KAKAO.getSocialName())
.clientId(clientId)
.clientSecret(clientSecret)
.authorizationGrantType(authorizationGrantType)
.redirectUri(redirectUri)
.scope(scope)
.tokenUri(tokenUri)
.clientName(KAKAO.getSocialName())
.userNameAttributeName("sub")
.jwkSetUri(jwkSetUri)
.userInfoUri(userInfoUri)
.issuerUri(issuerUri)
.clientAuthenticationMethod(clientAuthenticationMethod)
.authorizationUri(authorizationUri)
.build();
OidcUserRequest oidcUserRequest = new OidcUserRequest(clientRegistration, oAuth2AccessToken, oidcIdToken);
OidcUser oidcUser = oidcUserService.loadUser(oidcUserRequest);
Member byUsername = memberService.findByUsername(oidcUser.getName());
JwtTokenResponse token = jwtTokenProvider.genAccessTokenAndRefreshToken(byUsername);
boolean baekJoonConnect = false;
if (byUsername.getBaekJoonName() != null) baekJoonConnect = true;
return new SocialLoginResponse(token.accessToken(), token.refreshToken(), byUsername.getId(), baekJoonConnect);
}
private String[] scopeParsing(String scope) {
return scope.split(" ");
}
위 사진의 8번부터 과정입니다. Front-End 에서부터 받은 Kakao code 와 Redirect 정보로
Access Token 발급과 위에서 정의한 OidcService 를 호출하여 회원가입 을 진행합니다.
Front-end 에서 알아야 할 Access Token, Refresh Token, 회원 정보(id, 우리 서비스 연동여부)
를 return 해줍니다.
지금은 Kakao 만 제공하지만 google, naver 등 다른 서비스에 대한 확장성도 고려해야 한다고 생각합니다.
이를 위해 interface 와 abstract class 를 활용해보며 확장성 과 설계를 해보는 좋은 경험이었습니다.
하지만 OAuth2.0 은 많은곳에서 제공하는것에 비해 OIDC 프로토콜은 지원하지 않는 곳 이 많아 좀 더 고민을 해봐야할 것 같습니다.