🌠Development/SpringBoot

[Spring Security] CustomFilter를 만들고 UsernamePasswordAuthenticationFilter에 등록해보자

구동엽 2025. 5. 3. 14:08

 

1. 서론

Spring Security는 클라이언트의 요청이 여러개의 필터를 거쳐 DispathcerServlet(Controller)으로 향하는 중간 필터에서 요청을 가로챈 후 검증(인증/인가)을 진행한다.

 

클라이언트 요청 -> 서블릿 필터 -> 서블릿(Controller) 순으로 진행된다.

 

아래 사진을 참고하면 왼쪽(FilterChain)에서 DelegatingFilter를 등록하면 오른쪽(SecurityFilter)으로 가로채서 검증을 진행한다.

검증 진행 후 서블릿으로 요청을 전달한다.

  • SecurityFilterChain의 필터 목록과 순서 

 

1.2 Form 로그인 방식에서의 UsernamePasswordAuthenticationFilter

Form 로그인 방식에서는 클라이언트단이 username과 password를 전송한 뒤, SecurityFilter를 통과하게 되는데 이때 UsernamePasswordAuthenticationFilter에서 회원 검증을 시작한다.

회원 검증의 경우 UsernamePasswordAuthenticationFilter가 호출한 AuthenticationManager를 통해 검증을 진행하며 DB에서 조회한 데이터를 UserDetailsService를 통해 받는다.

 

1.3 JWT 방식에서의 UsernamePasswordAuthenticationFilter

JWT 방식에서는 Form 로그인 방식을 disable한다. 그 이유는 Form 로그인 방식은 Session 방식이기 때문에 disable한다.

따라서 Form 로그인 방식을 disable함에 따라 UsernamePasswordAuthenticationFilter가 작동하지 않는다.

 

그래서 우리는 CustomFilter를 만들어 CustomFilter를 UserPasswordAuthenticationFilter에 등록해야한다.

CustomFilter를 만들고 응답받은 request들을 AuthenticationManger에게 넘기는 단계까지 진행해보자.

 

2. CustomFilter 구현

CustomFilter.java

public class CustomFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;
    
    public CustomFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
        this.authenticationManager = authenticationManager;
        setFilterProcessesUrl("/v0/api/jwt/login");  // 원하는 엔드포인트로 변경
    }

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

        // 클라이언트 요청에서 userEmail, userPassword 추출
        String userEmail = obtainUsername(request);
        String userPassword = obtainPassword(request);

        // UsernamePasswordAuthenticationFilter에서 Authentication Manager에게 인증을 받을 때 전달할 DTO(UsernamePasswordAuthenticationToken)
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userEmail, userPassword, null);

        // token에 담은 검증을 위한 AuthenticationManager로 전달
        return authenticationManager.authenticate(authToken);
    }

    // 로그인 성공 시 실행하는 method (여기서 jwt 발급)
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication){

    }

    // 로그인 실패 시 실행하는 메소드
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed){

    }
}

 

기본 필터를 커스텀 해야하기 때문에 UsernamePasswordAuthenticationFilter를 상속받아 UsernamePasswordAuthenticationFitler의 메서드들( attemptAuthentication, successfulAuthentication, unsuccessfulAuthentication )을 Override를 하여 Custom 해준다.

 

2.1 attemptAuthentication 메서드

클라이언트(예: React, Postman)가 로그인 요청을 보낼 때 사용자 ID/비밀번호를 꺼내서 Spring Security가 이해할 수 있는 Authentication 객체로 만들어주는 메서드다. 이 메서드에서 실제 인증은 하지 않고, 토큰 객체만 만들어서 authenticationManager.authenticate()로 넘긴다.

 

2.2 successfulAuthentication 메서드

로그인 성공 시 실행되고 JWT 토큰을 새로 발급한다.

 

2.3 unsuccessfulAuthentication 메서드

로그인 실패 시 실행되는 메서드다.

 

2.4 로그인 엔드포인트 커스텀

Spring Security의 login 기본 경로는 /login 이다.

이를 우리가 원하는 경로로 커스텀 하기 위해서는 AuthenticationManager를 주입할 때 생성하는 CustomFitler의 생성자에 setFilterProcessesUrl을 호출하여 등록하면 된다.

    public CustomFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
        this.authenticationManager = authenticationManager;
        setFilterProcessesUrl("/v0/api/jwt/login");  // 원하는 엔드포인트로 변경
    }

3. CustomFilter 등록

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {

    // LoginFitler를 위한 Configuration 주입
    private final AuthenticationConfiguration authenticationConfiguration;

    // LoginFilter 커스텀 필터로 끌어오려면 명시적으로 AuthenticationManager를 Bean으로 등록해줘야한다.
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception{
        return configuration.getAuthenticationManager();
    }

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

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

        http
                .csrf((auth)->auth.disable());

        http
                .formLogin((auth)->auth.disable());

        http
                .authorizeHttpRequests((auth)->auth
                        .requestMatchers("/v0/api/jwt/login", "/", "/v0/api/jwt/join").permitAll()
                        .anyRequest().authenticated());

        // CustomFilter인 LoginFilter 등록
        http
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class);

        http
                .sessionManagement((session)->session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));


        return http.build();
    }
}