[OAuth 2.0] Spring Authorization Server 코드 분석(OAuth2AuthorizationEndpointFilter 클래스)

https://wo-dbs.tistory.com/348

 

[OAuth 2.0] 인가 엔드포인트 큰 흐름

인가 엔드 포인트를 개발하기 앞서 자세한 공부가 필요했으며 Spring Authorization Server의 코드를 분석하기 위해 큰 흐름을 다시 알 필요가 있었다. 다음 그림은 인가 엔드포인트의 순서대로 그림을

wo-dbs.tistory.com

 

큰 흐름에서의 분석

분석한 전체 그림

 

Spring Authorization Server 소스 코드에서 web 패키지의 OAuth2AuthorizationEndpointFilter의 클래스는 위 블로그에서 정리한 인가 엔드포인트 큰 흐름에서 순서 3번 18번까지 전체적인 흐름을 가지고 있다.

 

우선 OAuth2AuthorizationEndpointFilter 코드 중 doFilterInternal

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    //3qjs -
    //이 요청이 /oauth2/authorize 인가? -> 아니면 그냥 통과, 맞으면 OAuth 인가 처리 시작
    if (!this.authorizationEndpointMatcher.matches(request)) {
        filterChain.doFilter(request, response);
        return;
    }

    try {
        //HTTP 요청 → OAuth 요청 객체로 변환 즉, 파싱만 하는 거임
        // 	request.getParameter(“client_id”)
        //	request.getParameter(“redirect_uri”)
        //	request.getParameter(“response_type”)
        Authentication authentication = this.authenticationConverter.convert(request);
        if (authentication instanceof AbstractAuthenticationToken authenticationToken) {
            authenticationToken.setDetails(this.authenticationDetailsSource.buildDetails(request));
        }

        // OAuth 인가 로직의 시작 버튼
        // 여기서 Provider가 호출됨
        // Provider 안에서: client_id 검증, redirect_uri 검증 scope 검증, code 생성
        Authentication authenticationResult = this.authenticationManager.authenticate(authentication);

        // 유저가 로그인 안 돼 있으면 -> 로그인부터 하기 위해 Spring Security가 로그인 페이지로 보냄
        if (!authenticationResult.isAuthenticated()) {
            filterChain.doFilter(request, response); // 로그인 엔드포인트로 이동
            return;
        }

        // OAuth 인가 코드 플로우에서 사용자 동의(consent)가 필요한 경우를 처리하는 분기
        // 이 코드는 이 클라이언트가 scope들을 요청했는데 사용자가 아직 동의한 적이 없으면 -> 동의 화면으로 보내라라는 분기
        if (authenticationResult instanceof OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthenticationToken) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Authorization consent is required");
            }
            sendAuthorizationConsent(request, response,
                    (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication,
                    authorizationConsentAuthenticationToken);
            return;
        }

        this.sessionAuthenticationStrategy.onAuthentication(authenticationResult, request, response);

        //성공하면 redirect
        this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);

    }
    catch (OAuth2AuthenticationException ex) {
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(LogMessage.format("Authorization request failed: %s", ex.getError()), ex);
        }
        //실패하면 error
        this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
    }
}

 

Filter

Filter 부터 공부해보자

Filter는 HTTP 요청과 응답을 가로채서 컨트롤러에 도달하기 전/후에 공통 처리를 할 수 있는 관문

 

여기서 Filter를 쓰는 이유는

“/oauth2/authorize 요청이 들어왔을 때 OAuth 인가 흐름 전체를 ‘조율’하는 관문(오케스트레이터)”다. 즉, 요청을 받아서  누가 언제 무엇을 하게 할지 정한다

 

 

웹 요청 흐름을 아주 단순화하면

Client
 → Tomcat
 → Servlet Filter Chain   ← ★ Filter는 여기
 → DispatcherServlet
 → Controller
 → Service
 → Response

위와 같이 진행된다.

 

Filter는 Controller 보다 앞단, 요청/응답을 가로채서 뭔가 할 수 있는 위치

 

원본 코드인 OAuth2AuthorizationEndpointFilter 소스 파일의 역할을 다음과 같다

/oauth2/authorize 요청을 OAuth 인증 흐름으로 변환하는 전용 Filter

 

이 Filter 하나가 아래를 전부 담당함

  1. 이 요청이 OAuth 인가 요청인지 판별
  2. HTTP 요청을 OAuth 내부 객체로 변환
  3. 검증 로직에게 위임
  4. 성공 시 redirect
  5. 실패 시 RFC 규칙에 맞는 error 응답

 

이 Filter가 직접 하지 않는 것은 다음과 같다.

  • client_id 검증
  • redirect_uri 검증
  • scope 검증
  • code 생성
  • DB 조회

 

3번 브라우저의 GET or POST

브라우저 → 인가 서버

GET /oauth2/authorize?response_type=code&client_id=...

여기서부터 OAuth2AuthorizationEndpointFilter가 동작 시작

if (!authorizationEndpointMatcher.matches(request)) {
    filterChain.doFilter(request, response);
    return;
}
  • /oauth2/authorize 요청을 가로채는 첫 관문

 

4번 ~ 7번

4번 → 로그인 페이지(응답) - 브라우저와 인가 서버 통신

5번 → 브라우저의 이동(요청) - 브라우저와 인가 서버 통신

6번 → 인가 서버(응답) - 브라우저와 인가 서버 통신

7번 → 로그인 페이지 보임 - 리소스 소유자 && 브라우저

 

로그인 안 된 상태 → 로그인 페이지로 리다이렉트

// OAuth 인가 로직의 시작 버튼
// 여기서 Provider가 호출됨
// Provider 안에서: client_id 검증, redirect_uri 검증 scope 검증, code 생성
Authentication authenticationResult = this.authenticationManager.authenticate(authentication);

// 유저가 로그인 안 돼 있으면 -> 로그인부터 하기 위해 Spring Security가 로그인 페이지로 보냄
if (!authenticationResult.isAuthenticated()) {
	filterChain.doFilter(request, response); // 로그인 엔드포인트로 이동
	return;
}

“아직 로그인 안 됐네? → Security가 처리하게 넘긴다”

다음과 같은 일을 함

  • 302 /login
  • 로그인 HTML 응답
  • 사용자 로그인

→ 이건 Spring Security 쪽 책임

 

11번 - 브라우저가 자동으로 이동(요청) - 브라우저와 인가 서버 통신

  • 로그인 성공 후 다시 /oauth2/authorize로 복귀
GET /oauth2/authorize?... 
Cookie: JSESSIONID=...

이 시점에서:

  • 로그인됨
  • 다시 OAuth2AuthorizationEndpointFilter 통과

이 구간에서 Filter는 이렇게 행동함:

 

12 ~ 14번

12번 -> 동의 화면으로 리다이렉트 - 브라우저와 인가 서버 통신

13번 -> 브라우저가 동의 페이지 요청 - 브라우저와 인가 서버 통신

14번 -> 인가 서버 동의 HTML (응답) - 브라우저와 인가 서버 통신

// OAuth 인가 코드 플로우에서 사용자 동의(consent)가 필요한 경우를 처리하는 분기
// 이 코드는 이 클라이언트가 scope들을 요청했는데 사용자가 아직 동의한 적이 없으면 -> 동의 화면으로 보내라라는 분기
if (authenticationResult instanceof OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthenticationToken) {
	if (this.logger.isTraceEnabled()) {
		this.logger.trace("Authorization consent is required");
	}
	sendAuthorizationConsent(request, response,
			(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication,
			authorizationConsentAuthenticationToken);
	return;
}

이게 “동의 화면으로 리다이렉트” 구간

  • 302 /oauth2/consent
  • 또는 기본 HTML 동의 화면 응답

 

18번 - 인가 코드 발급! - 브라우저와 인가 서버 통신

동의 완료 → 인가 코드 발급 → 클라이언트로 리다이렉트

this.sessionAuthenticationStrategy.onAuthentication(authenticationResult, request, response);

//성공하면 redirect
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);

여기서 최종적으로

302 Location:
<https://client.example.com/callback?code=...&state=xyz>

인가 코드 발급의 마지막 문지기