Backend/SpringBoot

[Spring Security] JWT 사용시 Filter 구성 예

Dean83 2025. 12. 15. 16:00

JWT를 이용할 경우, 다음의 필터 구성을 한다.

  • JWT 검증 -> 로그인 + JWT 발급 -> 인가 -> 예외처리
  • UsernamePasswordAuthenticationFilter 앞에 JwtAuthenticationFilter 삽입
  • JWT 의 커스텀 필터 예
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

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

        String token = resolveToken(request);

        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication authentication =
                jwtTokenProvider.getAuthentication(token);

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

        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearer = request.getHeader("Authorization");
        if (bearer != null && bearer.startsWith("Bearer ")) {
            return bearer.substring(7);
        }
        return null;
    }
}

 

또한, userNamePasswordAuthenticationFilter 를 그대로 쓰지 않고, 수정하여 사용한다. 

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

    private final JwtTokenProvider jwtTokenProvider;

    public JwtLoginFilter(
        AuthenticationManager authenticationManager,
        JwtTokenProvider jwtTokenProvider
    ) {
        setAuthenticationManager(authenticationManager);
        setFilterProcessesUrl("/login");
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public Authentication attemptAuthentication(
        HttpServletRequest request,
        HttpServletResponse response
    ) {

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        UsernamePasswordAuthenticationToken authRequest =
            new UsernamePasswordAuthenticationToken(username, password);

        return getAuthenticationManager().authenticate(authRequest);
    }

    @Override
    protected void successfulAuthentication(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain chain,
        Authentication authResult
    ) throws IOException {

        String accessToken =
            jwtTokenProvider.createAccessToken(authResult);

        response.setContentType("application/json");
        response.getWriter().write("""
            {
              "accessToken": "%s"
            }
            """.formatted(accessToken));
    }
}

 

최종적으로 필터 등록을 하는 예는 다음과 같다. 

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final JwtTokenProvider jwtTokenProvider;
    private final AuthenticationConfiguration authConfig;

    public SecurityConfig(
        JwtAuthenticationFilter jwtAuthenticationFilter,
        JwtTokenProvider jwtTokenProvider,
        AuthenticationConfiguration authConfig
    ) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
        this.jwtTokenProvider = jwtTokenProvider;
        this.authConfig = authConfig;
    }

    /**
     * mvcMatcher 사용 시 반드시 필요
     */
    @Bean
    HandlerMappingIntrospector handlerMappingIntrospector() {
        return new HandlerMappingIntrospector();
    }

    @Bean
    SecurityFilterChain filterChain(
        HttpSecurity http,
        HandlerMappingIntrospector introspector
    ) throws Exception {

        AuthenticationManager authenticationManager =
            authConfig.getAuthenticationManager();

        JwtLoginFilter jwtLoginFilter =
            new JwtLoginFilter(authenticationManager, jwtTokenProvider);

        // mvcMatcher 빌더
        MvcRequestMatcher.Builder mvc =
            new MvcRequestMatcher.Builder(introspector);

        http
            // 기본 인증 방식 제거
            .formLogin(AbstractHttpConfigurer::disable)
            .httpBasic(AbstractHttpConfigurer::disable)
            .logout(AbstractHttpConfigurer::disable)

            // 세션, CSRF
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(sm ->
                sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )

            // 권한 설정
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(
                    PathRequest.toStaticResources().atCommonLocations()
                ).permitAll()

                .requestMatchers("/login").permitAll()

                // 🔐 SpEL ① 본인 리소스 접근만 허용
                .requestMatchers("/users/{id}")
                .access(new WebExpressionAuthorizationManager(
                    "#id == authentication.principal.id"
                ))

                // 🔐 SpEL ② ADMIN + 계정 활성화 사용자만 허용
                .requestMatchers("/admin/reports/**")
                .access(new WebExpressionAuthorizationManager(
                    "hasRole('ADMIN') and authentication.principal.enabled"
                ))

                // ===============================
                // ✅ RegexRequestMatcher 예제
                // ===============================
                // /api/v1, /api/v2, /api/v3 ... 버전 API
                .requestMatchers(
                    new RegexRequestMatcher(
                        "^/api/v[0-9]+/.*",
                        null
                    )
                ).hasRole("USER")

                // ===============================
                // ✅ MvcRequestMatcher 예제
                // ===============================
                // Spring MVC 매핑 규칙 그대로 사용
                .requestMatchers(
                    mvc.pattern(HttpMethod.GET, "/orders/{orderId}")
                ).hasRole("USER")

                .requestMatchers(
                    mvc.pattern("/admin/**")
                ).hasRole("ADMIN")

                .anyRequest().authenticated()
            )

            // 필터 등록 (순서 중요)
            .addFilterBefore(
                jwtAuthenticationFilter,
                UsernamePasswordAuthenticationFilter.class
            )
            .addFilterAt(
                jwtLoginFilter,
                UsernamePasswordAuthenticationFilter.class
            );

        return http.build();
    }
}

 

  • SpEL (Spring Expression Language)를 이용할 경우, 복합 권한을 체크할 수 있다.
    • 이 경우에는 .access() 를 이용한다.
  • mvc.pattern 을 통해 실제 컨트롤러에 매핑된 url을 이용할 수 있다 (인자값 까지 포함된 경우)
  • regex 의 경우, url 경로에 v숫자 가 들어가 있는 경우에 활용 할 수 있다.