프로젝트

블로그 만들기 - OAuth2 handler가 동작하지 않는 에러

ernest45 2025. 4. 3. 01:39

 

 

 

 

google로 OAuth2 로그인을 구현했으나,

 

 

 

로그인 시

 

문제 상황은 

 

 
기대 :  인증 후 /articles?token=<accessToken> 리다이렉트되길 원했지만,
 
 
실제 : 리다이렉트가 이루어지지 않고, 기본 경로(/) 이동
 
 
 
 

 

 

내가 지정해놓은 handler를 타지 않았다. 일반적인 실패라면 

failureHandler라도 탔을텐데 타지 않고 redirect 주소도 무시했다..

 

 

 

 

 

 

 

 

 

OAuth2 로그인 흐름 개요

 

 

 

총 크게 3번의 흐름으로 http 요청 및 응답 사이클이 돌아간다.

 

 

 

1. 클라이언트 → 서버 (OAuth2 로그인 요청)

주요 흐름:
1️⃣ 사용자가 /login 페이지에 접근
2️⃣ Spring Security가 /oauth2/authorization/google로 리다이렉트
3️⃣ 구글 로그인 페이지로 이동

 

 

2. 서버 → OAuth2 제공자 (Authorization 요청)

 주요 흐름:
1️⃣ 사용자가 구글에서 로그인 후 /login/oauth2/code/google?code=...로 리다이렉트
2️⃣ Spring Security가 Authorization Code를 받음
3️⃣ OAuth2LoginAuthenticationFilter가 Authorization Code를 사용해 Access Token 요청
4️⃣ OAuth2UserCustomService.loadUser()에서 사용자 정보 가져옴

 

 

3. 서버 내부 인증 처리 (Authentication 생성)

🔹 주요 흐름:
1️⃣ OAuth2UserCustomService에서 사용자 정보를 가져와 인증 객체 생성
2️⃣ OAuth2LoginAuthenticationFilter가 SecurityContextHolder에 저장
3️⃣ OAuth2SuccessHandler 실행

 

 

 

 

 

 

 

 

 

실제로 디버깅해보면서 어디가 에러인지 찾아보자ㅠㅠ

 

 

 

 

 

 

 

  1. 사용자가 로그인 페이지 접근 (/login)

  • http.oauth2Login().loginPage("/login") 설정에 의해, /login 요청 시 OAuth2 로그인 페이지가 노출됨.
  • 사용자가 로그인 버튼을 누르면, (Google)로 리다이렉트됨.

 

 

 

이 부분은 문제 없이 호출됨을 확인했다..

 

 

 

 

 

 2. OAuth2 제공자로 이동 (/oauth2/authorization/google)

  • 사용자가 로그인 버튼을 누르면 /oauth2/authorization/google 요청이 발생.
  • OAuth2AuthorizationRequestBasedOnCookieRepository에서 Authorization Request를 쿠키에 저장.
  • 구글 로그인 페이지로 리다이렉트됨.

 

 

 

 

log 값에 찍힌 것도 확인 했고,

 

"oauth2_auth_request"로 시작되는 path가 만들어진 걸 보고 Repository에는 정확히 저장되는 걸 확인했다.

 

 

 

 

 

 

 3. OAuth2 제공자에서 로그인 후 리다이렉트 (/login/oauth2/code/google)

 

  • 사용자가 구글 로그인 성공 시, Authorization Code를 포함하여 /login/oauth2/code/google로 리다이렉트됨.
  • Spring Security는 OAuth2LoginAuthenticationFilter를 통해 이 요청을 가로채서 처리.
  • OAuth2LoginAuthenticationFilter가 Authorization Code를 받아서 OAuth2 Access Token을 요청함.
  • 이후, Access Token을 사용하여 사용자 정보(UserInfo API) 요청을 보냄.

 

 

login/oauth2/code/google으로

 

 

 

 

 

redirect되는 것도 확인!

 

 

 

 

 

 

4. OAuth2UserService에서 사용자 정보 로드

  • OAuth2UserCustomService.loadUser()가 실행되어 사용자 정보를 가져옴.
  • 해당 사용자의 정보가 DB에 저장되어 있는지 확인하고, 없으면 새로 저장.

 

 

 

 

Oauth2User 객체로 들어온 값까지 저장 되는 것을 확인

 

 

save메서드

 

 

 

 

구글에서 보내 준 값으로 email과 nickname이 저장된다..

 

 

 

 5. Authentication 객체 생성 후 SecurityContext에 저장

  • OAuth2LoginAuthenticationFilter가 OAuth2UserService에서 받은 사용자 정보를 기반으로 OAuth2AuthenticationToken을 생성
  • SecurityContextHolder.getContext().setAuthentication(authentication);로 저장.

 

 

 

 

public class TokenAuthenticationFilter extends OncePerRequestFilter {
    private final TokenProvider tokenProvider;

    private final static String HEADER_AUTHORIZATION = "Authorization";
    private final static String TOKEN_PREFIX = "Bearer ";

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

        String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
        log.info("authorizationHeader", authorizationHeader);
        String token = getAccessToken(authorizationHeader);

        if (tokenProvider.validToken(token)) {

            Authentication authentication = tokenProvider.getAuthentication(token);

            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

 

TokenAuthenticationFilter는 JWT 기반 인증을 담당하는 필터로,
사용자의 요청에서 Authorization 헤더를 가져와

토큰을 검증하고 인증 정보를 SecurityContext에 저장하는 역할을 한다.

 

 

 

UsernamePasswordAuthenticationFilter 는 폼 기반 로그인 (ID/PW 로그인)필터이며,

Oauth2에선 사용하지 않는다.

 

그래서 UsernamePasswordAuthenticationFilter 전에

TokenAuthenticationFilter가 동작해야 인증된 사용자로 넘길 수 있다.

 

 

 

풀어서 설명하자면, 


TokenAuthenticationFilter
의 동작은 3번의 흐름 구글과의 http 통신 중 마지막에 확인된 사용자라면,

authorizaiton header Bearer 시작하는 토큰을 내려줄텐데 이 토큰이 유효하다면

 

 

SecurityContextHolder에 Authentication 객체 생성 및 저장하게 된다.!!!

 

 

 

 

 

 

그러나..

 

 

 

 

 

 

header 자체가 null 값이 계속 찍혔다..

 

 

 

그러니까 token이 검증되지 않고, SecurityContextHolder에 저장되지 않았다 ㅠㅠ

 

 

 

 

 

 

여기서 사실 실패를 한다면,,

 

 

여기서 failureHandler를 타야 한다고 생각했는데

그렇지 않고 redirect는 계속 되는 게 의아했다 그것도 원하지 않는 곳으로..

 

handler 두 자체를 타지 않아 어디서 잘못된 지 진짜 오래 헤맸다..

 

 

 

6. 에러의 원인

 

 

 

OAuth2SuccessHandler가 문제였다..

 

 

OAuth2SuccessHandler 에서

 

onAuthenticationSuccess() 메서드의 시그니처 문제가 원인이었다.

 

 

 

SimpleUrlAuthenticationSuccessHandler에서 override 하고 있는 메서드인

 

 

 

 

FilterChain이 없는  onAuthenticationSuccess()를 오버라이딩 해야하지만

 

 

FilterChain이 있는 onAuthenticationSuccess()를 재정의한 게 문제!

 

 

 

 

 

1)  정확한 onAuthenticationSuccess()  without.FilterChain

 

파라미터: HttpServletRequest, HttpServletResponse, Authentication
 
 
  • 추상 메서드로, 구현체에서 반드시 오버라이드해야 함.
  • 리다이렉트 또는 응답 처리 직접 수행하는 사용.
  • FilterChain 없으므로, 필터 체인을 진행할 필요가 없는 경우에 적합.
  • Spring Security의 인증 흐름에서 리다이렉트 필터 체인을 중단하려는 경우 사용.

 

 

handle를 따라가보면 redirect 시키는 걸 알 수 있다.

 

 

 

 

 

 

2) 다른 onAuthenticationSuccess()   with.FilterChain

 

파라미터: HttpServletRequest, HttpServletResponse, (FilterChain), Authentication.

 

 

  • 디폴트 메서드로, 구현체에서 오버라이드하지 않아도 동작.
  • 디폴트 구현: FilterChain 없는 onAuthenticationSuccess 메서드를 호출한 chain.doFilter 호출.
  • chain.doFilter 호출하여 필터 체인을 계속 진행 인증 성공 추가 필터 실행해야 하는 경우에 적합.
  • 리다이렉트를 수행하지 않고 필터 체인을 계속 진행하려는 경우 사용.

 

이걸 재정의했고, 결과적으론 

 

 

 

 

 

 

그럼 왜 ("/")로 redirect 됐을까?

 

 

예를 들어 4개의 클래스 및 인터페이스가 있다.

 

(a) = class OAuth2SuccessHandler

(b) = class SimpleUrlAuthenticationSuccessHandler(b)

(c) = interface AuthenticationSuccessHandler(c)

(d) = abstract class AbstractAuthenticationTargetUrlRequestHandler

 

 

 

내가 구현한 (a) OAuth2SuccessHandler는 (b) SimpleUrlAuthenticationSuccessHandler 을 상속받고 있다.

 

 

(b) SimpleUrlAuthenticationSuccessHandler은 (d) AbstractAuthenticationTargetUrlRequestHandler을 상속받고 있고,

 

(c) AuthenticationSuccessHandler을 구현하고 있다.

 

 

 

 

즉 나는(c) AuthenticationSuccessHandler의 실 구현체인 (b)의 메서드를 override 해서 사용해야 했지만,

(c)에 있는 default  메서드를 재정의해서 사용했고,

 

 

내가 만든 targerUrl가 아닌 "/"를 담게되고,

 

(조금 복잡한데, abstract의 handle() 을 호출하게 되어서 그렇다..)

 

 

 

그대로 redirect해서 8080/으로 302..

 

 

 

 

 

 

7. 성공 핸들러 (OAuth2SuccessHandler) 실행

  • OAuth2LoginAuthenticationFilter가 인증을 완료하면 OAuth2SuccessHandler.onAuthenticationSuccess() 호출.
  • 여기서 토큰을 생성하고, 리프레시 토큰을 저장한 후 클라이언트로 리다이렉트.

 

 

 

 

성공적으로 OAuth2SuccessHandler를 호출할 경우

 

 

principal에서 인증 정보를 가져와 토큰에 넣어주게 됨 !

 

 

위의 (isvaild(token)) 에 값을 넣어오게 할 수 있다 !!

 

 

 

리프레쉬 토큰이 쿠키에 잘 들어가는 걸 볼 수 있고,

 

 

 

 

그 결과!

 

 

 

 

 

 

 

 

 

 

내가 원하는 targetUrl인 articles로 redirect되는 걸 알 수 있다.

성공!!

 

 

 

 

 

 

 

 

 

 

 

 

 

진짜 에러 찾느라 고생했다..

 

 

 

 

 

 

 

 

사실 spring Security는 filter chain때문에 좀 더 복잡하게 돌아간다.

 

 

 

실제로 default 메서드를 재정의한 메서드를 호출하니 재정의한 메서드가 호출됐다..

그래서 내가 잘못 override 했어도 문제가 없다고 생각했으나

 

내가 위에 동작한 건 재정의한 메서드가 동작하지 않고 springSecurity에서 

(b) SimpleUrlAuthenticationSuccessHandler의 메서드를 사용해서 "/"가 호출됨을 확인했다.

 

 

 

(그 이유는 successhanlder를 주석 처리해도 똑같은 8080/으로 redirect됨을 확인했기에..)

 

 

 

 

 

 

 

미동작된 것이랑 같은 것처럼 로직이 동작해서 "/"로 redirect됨을 알 수 있다..

 

 

 

 

 

 

 

 

 

 

 

https://velog.io/@lsjbh45/Spring-Security-Authentication-%EC%9D%B4%ED%9B%84-%EC%B2%AB-%EC%9A%94%EC%B2%AD%EC%9D%B4-%EB%A9%94%EC%9D%B8-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%A1%9C%EC%9D%98-redirect%EB%A5%BC-%EA%B0%95%EC%A0%9C%ED%95%98%EB%8A%94-%EB%AC%B8%EC%A0%9C