들어가기에 앞서 프론트엔드 역할이 있음에도 Spring Security를 통해 소셜 로그인을 제공하면 백엔드의 역할이 너무 많다 생각해 해당 기능을 만들었습니다. 프론트엔드 일해라
우선 저의 프로젝트는 앱 기반 서비스였습니다.
앱 기반 서비스의 경우 OAuth를 사용할 때 프론트엔드에서 accessToken을 백엔드로 전송하는 방식을 주로 사용합니다. 하지만 Spring Security에서 제공하는 OAuth2는 OAuth2의 모든 절차를 백엔드에서 제공합니다.
Spring Security OAuth2의 동작 방식으로 인해 앱 서비스에서 사용하기에는 한계가 있었습니다.
그래서 AccessToken만을 이용해 백엔드에서 소셜 로그인을 처리하게 만들어 봤습니다.
우선 백엔드에서 소셜 로그인을 AccessToken만을 이용하여 진행하면 아주 간단해집니다!
소셜 로그인 순서로는
1. App(frontend)은 요청 헤더에 AccessToken을 포함해 서버(Backend)에 요청을 보냅니다.
2. 요청 받은 서버는 요청 헤더에 포함된 AccessToken을 통해 해당 소셜 로그인 Resource Server에 사용자 정보를 요청합니다.
3. Resource Server는 해당 AccessToken이 유효하면 사용자 정보를 반환합니다.
4. 서버에서 사용하는 인증/인가 방법(쿠키&세션, JWT)을 이용해 사용자 요청에 대해 응답합니다.
여기에 추가적으로 URL에 대상 소셜로그인 정보를 담아서 보내 여러 소셜로그인을 한 번에 제공할 수 있게 했습니다.
우선 구현할 클래스는 대략
1. 각 소셜로그인을 처리할 인터페이스 정의 및 구현(AuthHandler, XXXAuthHandler)
2. 사용자 요청에 대해 해당 소셜 로그인을 처리할 인스턴스 호출하는 클래스 정의(OAuthInvoker)
3. 요청 응답을 위해 사용하는 DTO(OAuthRequest, OAuthResponse) 정의
이 중 1,2에 해당하는 클래스를 소개할려 합니다.
우선 소셜 로그인 처리에 있어서 공통 기능을 생각해보면 두 개의 메소드로 생각할 수 있습니다.
1. 각 소셜 로그인 분기를 위해 처리 여부를 반환하는 메소드
2. Resource Server에서 사용자 정보를 받아오는 메소드
public interface AuthHandler{
OAuthUserInfo handle(OAuthRequest authenticationInfo); // Resource Server에서 사용자 정보 받아오는 메소드
default boolean isAccessible(OAuthRequest authenticationInfo){ // 소셜 로그인 처리 가능 여부 조회 메소드
return false;
}
}
구글 소셜로그인을 앞선 인터페이스를 이용해 구현해봤습니다. 그리고 구글 Resource Server에 사용자 정보를 요청을 FeignClient를 이용해 구현했습니다.
@Component
@RequiredArgsConstructor
public class GoogleOAuthHandler implements AuthHandler {
private static final Provider OAUTH_TYPE = Provider.GOOGLE;
private static final String GOOGLE_AUTHORIZATION_BEARER = "Bearer ";
private final GoogleOAuthFeignClient googleOAuthFeignClient; // Google Resource Server 요청 FeignClient
@Override
public OAuthUserInfo handle(OAuthRequest authenticationInfo) {
final String accessToken = authenticationInfo.getAccessToken();
final GoogleUserInfo googleUserInfo = getGoogleUserInfo(accessToken);
return new OAuthUserInfo(googleUserInfo.getName(), googleUserInfo.getEmail());
}
@Override
public boolean isAccessible(OAuthRequest authInfo) {
return OAUTH_TYPE.equals(authInfo.getProvider());
}
private GoogleUserInfo getGoogleUserInfo(final String accessToken){
return googleOAuthFeignClient.getGoogleUserInfo(GOOGLE_AUTHORIZATION_BEARER+accessToken);
}
}
해당 클래스는 요청에 대해 알맞는 AuthHandler 구현체에 요청을 보내주는 클래스 입니다.
해당 클래스는 다음과 같은 책임이 있습니다.
@Component
@RequiredArgsConstructor
public class OAuthInvoker {
private final List<AuthHandler> authHandlerList;
private final JWTProvider jwtProvider;
private final TokenSaveService tokenSaveService;
public OAuthResponse execute(OAuthRequest request){
OAuthUserInfo oAuthUserInfo = attemptLogin(request);
return generateServerAuthenticationTokens(oAuthUserInfo);
}
private OAuthUserInfo attemptLogin(OAuthRequest request) {
for (AuthHandler authHandler : authHandlerList) {
if (authHandler.isAccessible(request)) {
return authHandler.handle(request);
}
}
throw new AuthException(AuthError.OAUTH_FAIL);
}
private OAuthResponse generateServerAuthenticationTokens(OAuthUserInfo oAuthUserInfo) {
final JWTProvider.Token token = jwtProvider.generateToken(oAuthUserInfo.getEmail());
tokenSaveService.saveToken(token.refreshToken(), oAuthUserInfo.getEmail(), TokenType.REFRESH_TOKEN);
final String accessToken = AuthConsts.AUTHENTICATION_TYPE_PREFIX + token.accessToken();
final String refreshToken = AuthConsts.AUTHENTICATION_TYPE_PREFIX + token.refreshToken();
return OAuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
}
이러한 구성을 통해 새로운 소셜로그인 추가는 해당 소셜 로그인에 맞는 AuthHandler를 구현하여 빈으로 등록한다면 바로 새로운 소셜로그인을 제공할 수 있습니다.
마지막으로 해당 클래스들을 호출할 Controller, Service 코드는 다음과 같습니다.
@RestController
@RequiredArgsConstructor
public class OAuthController {
private final OAuthUseCase oAuthUseCase;
@GetMapping("/oauth/{provider}")
public OAuthResponse oAuthLogin(@PathVariable Provider provider, @RequestHeader("Authorization") String accessToken){
return oAuthUseCase.oAuthLogin(provider, accessToken);
}
}
@ApplicationService
@RequiredArgsConstructor
public class OAuthUseCaseImpl implements OAuthUseCase {
private final OAuthInvoker oAuthInvoker;
@Override
public OAuthResponse oAuthLogin(Provider provider, String accessToken){
return oAuthInvoker.execute(new OAuthRequest(provider, accessToken));
}
}