이제 실제 코드를 구현하면서 해당 흐름이 어떻게 되는지 살펴보자
직접 OAuth2 서버를 구현할 수도 있지만, 구글, 네이버, 카카오같이 훌륭한 인증 서버들이 존재한다. 해당 플랫폼에서는 OAuth2를 위한 서비스를 제공하고 있고 해당 플랫폼에 가입하여 등록할 수 있다.
카카오의 경우에도 친절하게 가이드를 안내하고 있다.

대부분의 플랫폼은 이렇게 redirect URI를 입력하게 되어있고, secret key를 발급받게된다. 이 두가지를 내 어플리케이션에서도 명시해줘야한다.
앞서 설명했던 흐름에따라 코드를 작성해보자. 아래 방식은 MSA에서 하는 방식이지만 빠른 테스트를 위해서 모놀리틱 방식으로 처리하였으므로, BFF 서버가 있지는 않고 애플리케이션에서 바로 인증하고 인증받은 코드를 Security를 이용해서 처리하도록 하겠다. Spring Security를 이용하면 OAuth2 처리 과정을 아주 손 쉽게 적용시킬 수 있다.
1️⃣ 프론트엔드가 로그인 요청을 하면, 인증서버에서 로그인 페이지로 리다이렉션(ex. 구글 로그인)
2️⃣ 로그인 인증시 ~client?code=xxx와 같이 authorization code를 보내주는데, 이 code를 BFF로 보낸다.
3️⃣ BFF는 인증서버로 해당 code를 전송하고 access token을 발급해준다.
4️⃣ BFF는 access token을 받아 내부에 저장한다.
5️⃣ 사용자가 프론트엔드에서 API를 요청하면, BFF가 내부적으로 토큰을 사용하여 리소스 서버와 통신한다.
6️⃣ 리소스 서버는 해당 토큰을 통해 인증서버와 통신하여 토큰을 검증 후 권한이 확인되면 필요한 데이터를 전송한다.
프론트엔드가 로그인 요청을 하면, 인증서버에서 로그인 페이지로 리다이렉션(ex. 구글 로그인) ▶️ 소셜 로그인 페이지로 리다이렉션하는 과정을 위해 Spring Security 적용을 해보자.
1) gradle에 oauth2-client를 적용해야한다.
implementation group: 'org.springframework.security', name: 'spring-security-oauth2-client', version: '필요버전'
2) yaml 이나 properties에 oauth2 설정을 해줄수있다.
spring:
security:
oauth2:
client:
registration:
google:
client-id: #발급받은 client id
client-secret: #발급받은 secret key
redirectUri: "http://localhost:8080/oauth2/callback/google"
scope:
- email
- profile
{baseUrl}/login/oauth2/code/{registrationId}로 되어있다고 한다.3) Security Config 설정
public class SecurityConfig {
...
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
...
//oaurh2 소셜로그인 적용
http.oauth2Login(login -> {
login.redirectionEndpoint(endpoint ->
endpoint.baseUri("/oauth2/callback/*"));
login.authorizationEndpoint(endpoint ->
endpoint.baseUri("/oauth2/authorize")
.authorizationRequestRepository(httpCookieOAuth2AuthorizationRequestRepository)
);
login.successHandler(oAuth2AuthenticationSuccessHandler);
login.failureHandler(oAuth2AuthenticationFailureHandler);
login.userInfoEndpoint(endpoint ->
endpoint.userService(userOAuth2Service));
});
...
}
Security Config에서 OAuth2 설정부분이다.
authorizationEndPoint는 프론트에서 해당 url로 인증서버에 요청을 보낼 수 있는 설정이다.
이때 내가 yaml에서 설정한 redirect url, client_id, scope 값을 참조하고 state값을 생성하여 인증서버로 요청한다. 그래서 인증서버의 로그인 페이지의 URL을 보면 내가 보낸 값들을 파라미터로 다시 보내주는 것을 볼 수 있다.
authorizationRequestRepository 는 인가 관련 정보를 저장하고 관리하는 역할을 하는 클래스이다. 여기서 scope나 redirect_uri 등을 저장하고 관리하는 역할을 하는데 이부분을 내가 커스텀한 클래스를 적용해줄 수 있다.
userInfoEndpoint 에서는 소셜 로그인 인증 후 인증서버에서 access token과 사용자 정보를 보내고 해당 정보를 내가 커스텀한 서비스에서 처리할 수 있도록 해준다.
redirectionEndpoint 는 yaml 에서 설정한 redirect url을 보고 인증서버가 해당 url로 access token과 정보를 보내므로 해당 api 응답을 받기위해 설정해야한다.
authorization code 처리와 access token 발급 ▶️ security는 이를 OAuth2LoginAuthenticationFilter에서 처리하게되는데 과정은 아래와 같다.
1) 소셜 로그인 페이지로 리다이렉트
2) 사용자가 로그인 완료
3) 소셜 인증 서버는 내가 설정한 (http://localhost:8080/oauth2/callback/*) URL로 리다이렉트
4) OAuth2LoginAuthenticationFilter가 요청을 가로채 인증 코드(Authorization code)로 access token 요청 -> 이때 내부적으로 OAuth2LoginAuthenticationProvider에서 소셜 서버와 통신한다.
5) access token을 받고 UserInfo 엔드포인트를 호출하여 사용자 정보 요청
6) UserInfoEndPoint에 설정된 userOAuth2Service가 사용자 정보 처리
7) 인증 성공 시 OAuth2AuthenticationSuccessHandler 호출
8) SuccessHandler에서 jwt토큰 생성 및 사용자 최종 목적지로 리다이렉트
6번 과정에서 UserOAuth2Service 구현 ▶️ DefaultOAuthUserService를 상속받아 loadUser를 오버라이딩 해주면 된다. 소셜 인증 서버에서 받아온 유저정보를 추출하여 나의 DB에 필요한 정보를 저장해주고, Security Context에 인증 객체에 사용될 수 있도록 OAuth2User 객체를 넘겨준다.
@Service
@RequiredArgsConstructor
@Slf4j
public class UserOAuth2Service extends DefaultOAuth2UserService {
private final MemberRepository memberRepository;
private final RoleRepository roleRepository;
private final MemberRoleRepository memberRoleRepository;
@Override
@Transactional
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
//User 정보를 저장하는 로직처리
return new DefaultOAuth2User(Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")),
oAuth2User.getAttributes(),
userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint()
.getUserNameAttributeName()
);
}
}
7번 과정에서 SuccessHandler 구현 ▶️ 성공 시에 보내줄 redirect url을 설정하고, jwt토큰을 생성하여 함께 넘겨준다. saveUserToken에서 jwt토큰을 생성하고, getRedirectUrl에 내가 보내주고 싶은 url과 token을 파라미터로 넘겨주면 된다.
@Component
@Slf4j
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtTokenProvider jwtTokenProvider;
private final AuthService authService;
private final MemberRepository memberRepository;
private final HttpCookieOAuth2AuthorizationRequestRepository cookieRepository;
private final String TARGET_URL = "http://localhost:8080/auth/oauth2/redirect";
@Override
@Transactional
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if(authentication instanceof OAuth2AuthenticationToken oauthToken) {
String targetUrl = determineTargetUrl(request, response, oauthToken);
JwtToken token = saveUserToken(oauthToken);
String redirectUrl = getRedirectUrl(TARGET_URL, token);
if (response.isCommitted()) {
logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
return;
}
getRedirectStrategy().sendRedirect(request, response, redirectUrl);
}
}
💡 OAuth2 서버가 발급해준 access token을 계속 써야할까?
나는 일반 로그인, 소셜로그인 두가지 방법을 사용해서 프로젝트를 만들었는데 이때 내부적으로 security에서는 인증(구글,카카오)서버에 access token을 요청하고 받아온다. 처음에는 이 access token을 그대로 header에 실어 보내주면 된다고 생각했다.
하지만 기간이 만료되면 계속해서 해당 access token을 인증서버에서 받아야 할 필요가 있을까 의문이 들었다. 계속해서 인증 서버와 통신하는것도 아닌것같고 내가 해당 토큰의 유효기간을 설정할 수도 없기 때문에 success handler를 통해서 내가 직접 구현한 access token을 보내주는 것이 맞는 방법이라는 생각이 들었다.
redirect url을 통해 받은 access token과 refresh token을 헤더에 넣어주고 프론트에서는 api 요청마다 access token을 실어보내주면 된다.
MSA에서는 각각의 애플리케이션마다 통신을 해야하기 때문에 각 애플리케이션에서 인증서버와 통신하며 토큰을 검증하고 권한 정보를 가져오는 처리를 해야한다.
💡 생각해볼 문제
현재 success handler에서 parameter로 access token과 refresh token을 보내주고 있는데, 이는 보안적으로 매우 취약하다.
사실은 send redirect 하는 과정에서 header에 실어서 보내주려고 시도했으나 리다이렉트의 경우(302) 헤더가 자동으로 포함되지 않는 문제가 생겨서 어쩔 수 없이 파라미터로 보냈다.
하지만 이를 좀 더 보안적인 측면에서 생각해서 파라미터가 아닌 다른 방법으로 넘기는 방식을 알아보는게 좋을 것 같다.