[패스트캠퍼스X야놀자 : 미니 프로젝트] Spring Security 및 OAuth 2.0를 이용한 소셜 로그인 구현하기 (1)

꼬마요리사레미·2023년 12월 20일

OAuth 2.0 Authorization Code Flow 과정 중 아래 과정 살피기

  • 사용자가 소셜 로그인을 통한 인증을 시도할 때, 해당 사용자의 정보에 접근하기 위한 권한 즉, Authorization Code 를 얻기 위해 OAuth 2.0 Authorization Server 로 Redirect 되는 과정
  • 사용자가 개인 정보 수집에 대한 동의를 한 상태에서 로그인을 성공적으로 마쳤을 때, 등록한 OAuth Redirect URI 에 Authorization Code 를 포함하여 서버로 리다이렉트 되고, 해당 Authorization Code 를 Access Token 으로 교환하는 과정
public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
    // ...
    private OAuth2AuthorizationRequestResolver authorizationRequestResolver;
    // ...
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
            if (authorizationRequest != null) {
                this.sendRedirectForAuthorization(request, response, authorizationRequest);
                return;
            }
        } catch (Exception ex) {
            this.unsuccessfulRedirectForAuthorization(request, response, ex);
            return;
        }
        // ...
    }  
    // ...
}
  1. 사용자 소셜 로그인 시도 시 브라우저 → 백엔드 서버 로 다음과 같이 요청을 보낸다.
    GET /oauth2/authorization/{registrationId}
  2. Spring Security 의 OAuth2AuthorizationRequestRedirectFilter 클래스가 해당 요청을 감지한 후 처리한다.

✨ OAuth2AuthorizationRequestRedirectFilter Class

  • Spring Security 에서 OAuth 2.0 Authorization Request 를 처리하는 필터이다.
  • 주로 Authorization Code Grant Flow 및 Implicit Grant Flow와 같은 OAuth 2.0 인증 프로토콜에서 사용된다.

Authorization Code Grant Flow vs Implicit Grant Flow

  1. Authorization Code Grant Flow : 보안 측면에서 강력하며, 클라이언트는 인가 코드를 받은 후에만 액세스 토큰을 요청할 수 있다.
  2. Implicit Grant Flow : 액세스 토큰이 바로 리다이렉션 URL의 프래그먼트에 포함되어 반환되며, 인가 코드를 교환하는 추가 단계가 없다. 보안 측면에서는 Authorization Code Flow보다 취약할 수 있다.
  • 주요 목적은 사용자에게 로그인 및 동의 페이지로 리다이렉트하고, OAuth 2.0 Authorization Server 로부터 Authorization Code 를 부여받는 프로세스를 시작하는 것이다.

💡 doFilterInternal Method

  • authorizationRequestResolver 의 resolve 메서드를 호출하여 Authorization Request 를 위한 OAuth2AuthorizationRequest 객체를 생성한다.
  • Authorization Request 객체를 이용하여 sendRedirectForAuthorization 메서드를 호출하고 OAuth 2.0 Authorization Server 로 Redirect 를 수행한다.
public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {

    private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";

    private final ClientRegistrationRepository clientRegistrationRepository;
    private final AntPathRequestMatcher authorizationRequestMatcher;

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        String registrationId = this.resolveRegistrationId(request);
        if (registrationId == null) {
            return null;
        }
        String redirectUriAction = getAction(request, "login");
        return resolve(request, registrationId, redirectUriAction);
    }

    private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId,
            String redirectUriAction) {
        if (registrationId == null) {
            return null;
        }
        ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
        if (clientRegistration == null) {
            throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
        }
        Map<String, Object> attributes = new HashMap<>();
        attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
        OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration, attributes);

        String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);

        builder
            .clientId(clientRegistration.getClientId())
            .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
            .redirectUri(redirectUriStr)
            .scope(clientRegistration.getScopes())
            .state(this.stateGenerator.generateKey())
            .attributes(attributes);

        this.authorizationRequestCustomizer.accept(builder);

        return builder.build();
    }

    private String resolveRegistrationId(HttpServletRequest request) {
        if (this.authorizationRequestMatcher.matches(request)) {
            return this.authorizationRequestMatcher.matcher(request).getVariables()
                    .get(REGISTRATION_ID_URI_VARIABLE_NAME);
        }
        return null;
    }
}

✨ DefaultOAuth2AuthorizationRequestResolver Class

  • OAuth2AuthorizationRequestResolver 인터페이스의 구현체이다. 즉, Spring Security 에서 OAuth 2.0 Authorization Request 를 해석하고 생성하는 클래스이다.
  • 사용자로부터 소셜 로그인 요청이 들어왔을 때 OAuth 2.0 Authorization Server 로 Redirect 하기 위한 OAuth 2.0 Authorization Request 객체를 구성하는 역할을 담당한다. ( 이는 URL 및 Parameter를 구성하는 데 사용된다. )

💡 resolveRegistrationId Method

  • 주어진 요청이 특정 패턴과 일치하는 지 확인한 후 일치할 경우 해당 패턴에서 추출한 registrationId 를 반환한다.
    authorizationRequestMatcher = /oauth2/authorization/{registrationId}

💡 resolve Method

  • ClientRegistrationRepository 에는 application.yml 파일에서 설정한 OAuth 2.0 Client Registration 정보가 들어있다.
  • registrationId 에 해당하는 OAuth 2.0 Client Registration 객체를 가져온다.
  • 이를 활용하여 OAuth 2.0 Authorization Request 객체를 구성하고 반환한다.
public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
    // ...
	private RedirectStrategy authorizationRedirectStrategy = new DefaultRedirectStrategy();
	private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository();
    // ...
	private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
			OAuth2AuthorizationRequest authorizationRequest) throws IOException {
		if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {
			this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
		}
		this.authorizationRedirectStrategy.sendRedirect(request, response,
				authorizationRequest.getAuthorizationRequestUri());
	}
	// ...
}

💡 sendRedirectForAuthorization Method

  • 생성된 OAuth 2.0 Authorization Request 객체를 통해 Redirection 를 위한 작업을 수행한다.
  • authorizationRequestRepository 의 saveAuthorizationRequest 메서드를 호출하여 OAuth 2.0 Authorization Request 객체를 HttpSession 에 저장한다.
  • 이후 authorizationRedirectStrategy 의 sendRedirect 메서드를 통해 백엔드 서버 → 브라우저 → OAuth 2.0 인가 서버 의 흐름으로 Redirection 을 수행한다.
GET authorization_uri?
    response_type=code
    &client_id=my_client_id
    &redirect_uri=my_redirect_uri
    &scope=scope1 scope2
    &state=random_string

✋ state parameter

  • CSRF (Cross-Site Request Forgery) 공격을 방지하기 위한 보안 목적의 매개변수로 사용된다.
  • 이 매개변수는 랜덤하게 생성된 문자열로 구성되어 있으며, 클라이언트가 인증 요청을 시작할 때 생성되어 응답 받을 때까지 변하지 않는다.
  • 인증 요청을 보낸 클라이언트는 응답을 받을 때 해당 매개변수를 확인하여 요청과 응답이 서로 일치하는지 확인한다.
  • 이를 통해 중간자 공격 등으로 인한 보안 문제를 방지할 수 있다.
public final class HttpSessionOAuth2AuthorizationRequestRepository
        implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
        
    private static final String DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME = HttpSessionOAuth2AuthorizationRequestRepository.class
			.getName() + ".AUTHORIZATION_REQUEST";

	private final String sessionAttributeName = DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME;  
    // ...    
    @Override
    public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest,
                                         HttpServletRequest request, HttpServletResponse response) {
        Assert.notNull(request, "request cannot be null");
        Assert.notNull(response, "response cannot be null");
        if (authorizationRequest == null) {
            removeAuthorizationRequest(request, response);
            return;
        }
        String state = authorizationRequest.getState();
        Assert.hasText(state, "authorizationRequest.state cannot be empty");

        request.getSession().setAttribute(this.sessionAttributeName, authorizationRequest);
    }
    // ...   
}

✨ HttpSessionOAuth2AuthorizationRequestRepository Class

  • OAuth 2.0 Authorization Request 정보를 관리하기 위한 기본적인 구현을 제공하는 클래스이다.
  • OAuth2AuthorizationRequestRepository 인터페이스를 구현하고 있어, OAuth 2.0 Authorization Request 정보를 세션에 저장하고 검색하는 역할을 수행한다.

💡 saveAuthorizationRequest Method

  • OAuth 2.0 Authorization Request 객체에 저장된 정보를 세션에 저장한다.

HttpSession과 JSESSIONID

  1. 클라이언트가 웹 애플리케이션에 최초로 접속했을 때 톰캣과 같은 서블릿 컨테이너가 새로운 세션을 생성하고 그 세션에 대한 고유한 식별자인 JSESSIONID를 생성한다.
  2. 서버는 JSESSIONID를 클라이언트에게 쿠키로 전송한다. 이 쿠키는 클라이언트의 브라우저에 저장되어 나중에 클라이언트가 서버에 요청을 보낼 때마다 함께 전송된다.
  3. 서버는 JSESSIONID를 사용하여 클라이언트의 세션을 식별하고, 클라이언트에게 연관된 세션 데이터를 유지한다.
  • 세션에서 OAuth 2.0 Authorization Request 에 관한 데이터를 저장하고 검색할 때 사용되는 키 값으로 sessionAttributeName 을 지정한다.
  • OAuth 2.0 Authorization Server 로부터 부여받은 Authorization Code 를 Access Token 으로 교환하기 위한 단계에서 사용될 수 있다.

  • 이후 사용자에게 로그인 페이지를 표시하게 된다. 사용자는 OAuth 2.0 인가 서버에서 자신의 계정 정보로 로그인하고, 해당 어플리케이션이 사용자의 정보에 접근하기 위한 권한 부여를 승인 또는 거부할 수 있다.
  • 사용자 인증 완료 시 OAuth 2.0 인가 서버 → 브라우저 → 백엔드 서버의 흐름으로 리다이렉트를 수행한다.
    GET redirect_uri?code=authorization_code&state=state_value
  • 이후 OAuth2LoginAuthenticationProvider 의 authenticate 메서드가 실행되는데, 이는 Authorization Code 를 통해 Access Token 을 교환하고, 교환된 Access Token 을 사용하여 사용자 정보를 가져오는 역할을 수행한다.
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
    // ...
	private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;
	private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
	private GrantedAuthoritiesMapper authoritiesMapper = ((authorities) -> authorities);
	// ...
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken) authentication;
		if (loginAuthenticationToken.getAuthorizationExchange().getAuthorizationRequest().getScopes()
				.contains("openid")) {
			return null;
		}
		OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
		try {
			authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
					.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
							loginAuthenticationToken.getClientRegistration(),
							loginAuthenticationToken.getAuthorizationExchange()));
		}
		catch (OAuth2AuthorizationException ex) {
			OAuth2Error oauth2Error = ex.getError();
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
		}
		OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
		Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
		OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
				loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
		Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
				.mapAuthorities(oauth2User.getAuthorities());
		OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
				loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(),
				oauth2User, mappedAuthorities, accessToken, authorizationCodeAuthenticationToken.getRefreshToken());
		authenticationResult.setDetails(loginAuthenticationToken.getDetails());
		return authenticationResult;
	}
}

✨ OAuth2LoginAuthenticationProvider Class

  • AuthenticationProvider 인터페이스를 구현한 클래스 중 하나이다.
  • OAuth 2.0 Loign 을 이용한 Authentication 정보를 제공하는 클래스로, Authorization Code → Access Token Exchange 작업을 수행하고, Access Token → Resoure Exchange 작업을 수행한다.
  • 최종적으로 사용자 인증이 완료되었을 때 완전한 Authentication 정보가 담긴 OAuth2.0 Login Authentication Token 객체를 생성하여 반환한다.

✨ 흐름 최종 정리 ✨

  1. 사용자가 애플리케이션에서 소셜 로그인 시도 시 Client Server 는 OAuth 2.0 Authorization Code 를 얻기 위해 OAuth 2.0 Authorization Server 로 Redirection 시킨다.
  2. 사용자는 OAuth 2.0 Authorization Server 에서 로그인하고 권한 부여를 허용한다.
  3. 로그인을 통한 인증 성공 시 OAuth 2.0 Authorization Server 는 OAuth 2.0 Authorization Code 정보를 포함하여 다시 Client Server 로 Redirection 시킨다.
  4. Client Server 는 OAuth 2.0 Authorization Code 를 사용하여 다시 OAuth 2.0 Authorization Server 로부터 Access Token 을 교환하는 작업을 수행한다.
  5. Client Server 는 Access Token 을 사용하여 Resoure Server 로부터 OAuth 2.0 User 정보를 가져오고, 최종적으로 OAuth 2.0 Login Authentication Token 을 생성하여 Authentication 정보를 SecurityContextHolder 에 저장한다.
  • 위의 과정 중 Access Token 취득 후 직접 커스텀한 CustomOAuth2UserService 클래스의 loadUser 메서드가 호출되고, 데이터베이스 조회를 통해 현재 사이트에 가입된 사용자인 지 판단 후 회원가입 또는 회원 정보 갱신 로직을 수행시키며, 최종적으로 Spring Security 가 인증 여부를 확인하여 JWT 를 발급을 할 수 있도록 OAuth2User 객체를 반환한다.

0개의 댓글