본문 바로가기
프로젝트

My Record - jwt인증과 전체 필터 흐름과 config 뜯어보기

by ernest45 2025. 4. 1.

 

 

cofig 설정들

package me.hanjun.config;

import lombok.RequiredArgsConstructor;
import me.hanjun.config.oauth.Oauth2UserCustomService;
import me.hanjun.repository.RefreshTokenRepository;
import me.hanjun.service.UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import static org.springframework.boot.autoconfigure.security.servlet.PathRequest.toH2Console;

@Configuration
@RequiredArgsConstructor
public class WebOauthSecurityConfig {

    private final Oauth2UserCustomService oauth2UserCustomService;
    private final TokenProvider tokenProvider;
    private final RefreshTokenRepository refreshTokenRepository;
    private final UserService userService;


    @Bean
    public WebSecurityCustomizer configure() {
        //스프링 시큐리티 기능 비활성화
        return (web -> web.ignoring()
                .requestMatchers(toH2Console())
                .requestMatchers("/img**", "/css/**", "js/**"));
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {


        //토큰 방식으로 인증을 하기 때문에 기존에 사용하던 폼로그인, 세션 비활성화
        http.csrf().disable()
                //CSRF(사이트 간 요청 위조) 보호 비활성화. 토큰 기반 인증(JWT)에서는 세션이 없으므로 CSRF 공격 위험이 낮아서 꺼둠.
                .httpBasic().disable() //HTTP Basic 인증(브라우저 팝업 로그인) 비활성화
                .formLogin().disable() //폼로그인 비활성화
                .logout().disable(); // 아래에서 커스터마이징.


        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //SessionCreationPolicy.STATELESS: 세션을 사용하지 않음.
        //왜?: JWT 토큰으로 인증하니까 서버가 상태(세션)를 유지할 필요 없음. 클라이언트가 토큰을 헤더에 실어 보냄.

        //헤더를 확인할 커스텀 필드 추가
        http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        //addFilterBefore(...UsernamePasswordAuthenticationFilter.class):
        // Spring Security의 기본 인증 필터(UsernamePasswordAuthenticationFilter) 전에 실행.

        // 토큰 재발급 URL는 인증 없이도 접근 가능하도록 설정, 나머지 API URL은 인증 필요
        http.authorizeHttpRequests()
                .requestMatchers("/api/token").permitAll()
                .requestMatchers("/api/**").authenticated()
                .anyRequest().permitAll();

        http.oauth2Login()
                .loginPage("/login")
                .authorizationEndpoint()
                // Authorization 요청과 관련된 상태 저장
                .authorizationRequestRepository(OAuth2AuthorizationREquestBasedOnCookieRepository())
                .and()
                .successHandler(oAuth2SuccessHander()) // 인증 성공 시 실행할 핸들러
                .userInfoEndpoint()
                .userService(oauth2UserCustomService);

        http.logout()
                .logoutSuccessUrl("/login");

        // api로 시작하는 uri인 경우 401 상태 코드를 반환하도록 예외처리
        http.exceptionHandling()
                .defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)
                        , new AntPathRequestMatcher("/api/**"));

        return http.build();
    }

    @Bean
    public OAuth2SuccessHandler oAuth2SuccessHandler() {
        return new OAuth2SuccessHandler(tokenProvider,
                refreshTokenRepository, oAuth2AuthorizationREquestBasedOnCookieRepository(),
                userService);
    }

    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter(tokenProvider);
    }

    @Bean
    public OAuth2AuthorizationREquestBasedOnCookieRepository
    oAuth2AuthorizationREquestBasedOnCookieRepository() {

        return new OAuth2AuthorizationREquestBasedOnCookieRepository();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

jwt로 인증 시 인증 필터
package me.hanjun.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
@Slf4j
@RequiredArgsConstructor
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);

        String token = getAccessToken(authorizationHeader);

        if (tokenProvider.validToken(token)) {

            Authentication authentication = tokenProvider.getAuthentication(token);

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

        filterChain.doFilter(request, response);
    }

    private String getAccessToken(String authorizationHeader) {
        if (authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)) {
            return authorizationHeader.substring(TOKEN_PREFIX.length());
        }

        return null;
    }
}
 

usernamepassowordauthenticationfilter 전에 작동할 만든 tokenAuthenticationFilter

 

 

 

 

Spring Security 필터 체인 구성

 
말했듯이 Spring Security는 요청을 처리하기 위해 여러 필터를 순차적으로 실행
 
그전에 필터에 대해 정리한 이 글부터 보고 오면 좋다!

 

 
   
 

Spring Security는  필터 체인(SecurityFilterChain) 을 통해 보안 로직을 실행함.

 

 

 

     예를 들어 여러 필터들이 있고
  1. CsrfFilter: CSRF 토큰 검사.
  2. UsernamePasswordAuthenticationFilter: 폼 로그인 처리.핵심이다! 이 전에 로그인이 되어야 한다.
  3. BasicAuthenticationFilter: HTTP Basic 인증 처리.
  4. ExceptionTranslationFilter: 예외 처리. 등등...

 

 

 

 

1. addFilterBefore란?

 

 

 

순차적으로 실행하는 필터먼저 사용할 내가 만든 커스텀를 끼워 넣기 가능!

 

tokenAuthenticationFilter(): 전에 만들 jwt토큰 확인 후 저장하는 커스텀 필터

 

 

(tokenAuthenticationFilter의 핵심 로직)

 

 

 

addFilterBefore의 동작

tokenAuthenticationFilter(): 우리가 만든 커스텀 필터(JWT 토큰을 확인하는 로직).
 
  • UsernamePasswordAuthenticationFilter.class: Spring Security의 기본 필터 중 하나로, 폼 로그인 요청(예: /login에 POST로 사용자 이름/비밀번호 전송)을 처리.
  • 설정 결과:
    • tokenAuthenticationFilterUsernamePasswordAuthenticationFilter 전에 실행

 

 

 

 

 

JWT 인증을 먼저 처리하려면 순서를 이렇게 바꿔야함!

 

예를 들어, 요청이 오면

  1.  tokenAuthenticationFilter: 토큰 있는지 또는 유효한지 확인
  2.  UsernamePasswordAuthenticationFilter:  폼 로그인 체크 (여기선 비활성화됐지만)
근데 왜 앞에 넣냐? JWT로 인증하면 세션이나 폼 로그인이 필요 없으니까, 커스텀한 필터로
제일 먼저 토큰을 보고 인증 확인 하려고!
 
 
 
 

이 순서가 뒤바뀌면 기본 로그인부터 시도하고 인증하고 넘겨버릴 수도 있음!

 

 

 

** 번외 **

 

사실 UsernamePasswordAuthenticationFilterform.login.disable(). 비활성화 시

UsernamePasswordAuthenticationFilter가 FilterChain에 등록되지 않는다!!!

 

근데 이럴 경우에도

http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); 쓰면

 

tokenAuthenticationFilter는 UsernamePasswordAuthenticationFilter 앞에

잘 등록된다.

 

그 이유가 뭘까?

 

 

 

 

간단히 말하자면, 필터의 존재에 고유한 각 순서 값을 가진다.!

(FilterOrderRegistration에서 관리함)

실제로 필터에 등록되지 않았더라도 그 순서 값을 여전히 참조 값으로 잡을 수 있음!!

 

 

 

실제 순서가 추가되는 모습

 

 

 
 
 
 
 

2. OAuth2AuthorizationRequestBasedOnCookieRepository?

 

 

Spring Security는 세션을 기반으로 인증을 관리함! but

 

 

 

  • STATELESS로 설정하면 Spring Security가 HTTP 요청 간의 세션을 생성하거나 유지x
  • 즉, 서버가 클라이언트의 세션을 저장하지 않으므로 세션 기반 인증(로그인)을 사용불가
  • 보통 JWT(JSON Web Token) 인증 방식에서 사용됨.
 
 
 
 

 

 

jwt를 활용하기 위해서 security의 session 정책을 아예 사용하지 않는다는 뜻!

 

 

2-1. 내가 로그인을 시작한 위치는 어떻게 저장하지? STATELESS인데?

 

 

로그인 시작   → 세션에 상태 저장 → 구글로 리다이렉트 → 돌아오면 세션에서 상태 꺼냄

 

 

기존 세션 사용 시 
 
 
*HttpSessionOAuth2AuthorizationRequestRepository를 써서 OAuth2AuthorizationRequest를 세션에 저장*
 
 
 
 
  로그인 시작 → 세션에 상태 저장 → 구글로 리다이렉트 → 돌아오면 세션에서 상태 꺼냄.
 
 
 
  • 근데  STATELESS로 설정했으니까 세션이 없다 그래서 세션 대신 쿠키에 저장하도록 커스터마이징

 

 

 

 

 

그래서 쿠키 사용 시

쿠키 저장소를 따로 만들고
쿠키에 저장하는 메서드!
 
  1. 구글로 로그인" 누르면 saveAuthorizationRequest가 쿠키에 상태 저장
  2. 구글에서 돌아오면 loadAuthorizationRequest로 쿠키에서 상태 읽음.

 

 

 

 

 

Q) 그럼 세션저장소를 완전히 못쓰는 건가?

 

A) NO !

 

코드에서 HttpSession을 직접 사용하여 값을 저장하는 것은 가능하지만, Spring Security가 관리하는 인증 관련 세션을 사용하지 않음.

 

 

 

 
 
 
 
(번외 - 세션 정책 옵션)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

--번외--

 

tokenAuthenticationFilter에서 doFilter 호출 시 메서드 파라미터는 2개지만,

 

UsernamePasswordAuthenticationFilter의 실제 구현된 doFilter는 3개로 받는다?

그중에 내가 넘긴 jwt에서는 dofilter의 메서드 파라미터 불일치가 있었는데 궁금했다.

 

 
항상 dofilter를 호출 땐 필터체인을 받지 않고, 
받을 땐 필터체인까지 받아야한다!!
 
실제 인터페이스를 그렇게 설계해서!
 
(chain까지 명시적으로  넘길려고 했더니만 에러남)

 

 

 

실제 FilterChain 내부 동작 
 
filterChain FilterChainProxy가 만든 VirtualFilterChain이고,
여기서 추가해서 넘기게 돼서 메서드 
 

 
 
 
 
 

실제 추가 로직

 

 

 

 

 

[Spring Security] 스프링시큐리티 설정값들의 역할과 설정방법(2)

스프링시큐리티의 여러가지 설정값들의 역할과 설정방법을 상세히 알아봅니다. Spring Security 커스텀 필터를 이용한 인증 구현 - 스프링시큐리티 설정(2) 본 포스팅은 스프링시큐리티의 전반적인

kimchanjung.github.io