본문 바로가기

백엔드 스터디

스프링 OAuth2 로그인 구현하기 3 - JWT

 

Spring Security + JWT로 로그인 구현하기 (1) - 커스텀 필터 만들기

이번에는 세션 방식에서 JWT를 사용한 인증 방식을 구현해 볼 예정입니다. 이를 위해 SecurityConfig에서 formLogin을 비활성화했는데 이렇게 하면 기존의 로그인 처리를 담당하던 UsernamePasswordAuthenticationFilter가 동작하지 않게 됩니다.

따라서 우리는 JWT 방식에 맞는 새로운 로그인 처리 필터를 직접 만들고 등록해야 합니다.

 목표 

  1. ID, PW 검증을 위한 커스텀 필터 작성
  2. DB 회원 정보를 기반으로 한 검증 로직 작성
  3. 로그인 성공 시 JWT를 반환할 SuccessHandler 구현
  4. 작성한 커스텀 필터를 SecurityConfig에 등록

이번 포스팅에서는 1번과 4번, 즉 커스텀 필터를 만들고 등록하는 과정을 기록해 보겠습니다.


1단계: 로그인 요청을 가로챌 커스텀 필터 작성

Spring Security는 Form 로그인 요청이 오면 UsernamePasswordAuthenticationFilter를 통해 ID와 비밀번호를 검증합니다. 우리는 이 필터를 상속받아 우리만의 LoginFilter를 만들어 보겠습니다.

LoginFilter.java

public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;

    public LoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    // 로그인 요청 시 동작하는 메소드
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

		// 1. 클라이언트 요청에서 username, password 추출
        String username = obtainUsername(request);
        String password = obtainPassword(request);

		// 2. Spring Security에서 검증하기 위해 token에 담기
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);

		// 3. 검증을 위해 AuthenticationManager로 전달
        return authenticationManager.authenticate(authToken);
    }

	// 로그인 성공 시 실행되는 메소드 (여기서 JWT를 발급)
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
        System.out.println("로그인 성공!"); // 임시 출력
    }

	// 로그인 실패 시 실행되는 메소드
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
        System.out.println("로그인 실패!"); // 임시 출력
    }
}
  • attemptAuthentication(): 로그인 요청을 가로채는 가장 핵심적인 메소드입니다.
    1. 사용자가 입력한 username과 password를 추출합니다.
    2. 인증을 진행해 줄 AuthenticationManager에게 넘겨주기 위해 UsernamePasswordAuthenticationToken 객체를 생성합니다.
    3. authenticationManager.authenticate()에 token을 담아 넘겨주면, 내부적으로 DB의 회원 정보와 비교하는 인증 절차가 진행됩니다. (이 부분은 다음 포스팅에서 구현할 예정입니다.)
  • successfulAuthentication(): 인증이 성공했을 때 호출됩니다. 바로 이곳에서 JWT를 생성하여 클라이언트에게 응답해주면 됩니다.
  • unsuccessfulAuthentication(): 인증이 실패했을 때 호출됩니다.

2단계: 커스텀 필터를 SecurityConfig에 등록하기

이제 우리가 만든 LoginFilter를 Spring Security의 필터 체인에 등록할 차례입니다.

SecurityConfig.java

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // AuthenticationManager를 만들기 위해 AuthenticationConfiguration 주입
    private final AuthenticationConfiguration authenticationConfiguration;

    public SecurityConfig(AuthenticationConfiguration authenticationConfiguration) {
        this.authenticationConfiguration = authenticationConfiguration;
    }

    // AuthenticationManager를 Bean으로 등록
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

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

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

        // CSRF, FormLogin, HttpBasic 인증 방식 비활성화
        http
                .csrf((auth) -> auth.disable());
        http
                .formLogin((auth) -> auth.disable());
        http
                .httpBasic((auth) -> auth.disable());

        // 요청 경로별 인가 설정
        http
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/login", "/", "/join").permitAll() // 이 경로들은 모두 허용
                        .anyRequest().authenticated()); // 나머지는 인증 필요

        // ⭐ 우리가 만든 커스텀 필터(LoginFilter)를 등록
        // addFilterAt()은 특정 필터 자리에 새로운 필터를 추가하는 메소드입니다.
        // 기존 UsernamePasswordAuthenticationFilter 자리에 LoginFilter를 등록합니다.
        http
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class);

        // 세션을 stateless 상태로 설정 (JWT 사용을 위함)
        http
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}
  • AuthenticationManager Bean 등록: LoginFilter는 생성자에서 AuthenticationManager를 필요로 합니다. 따라서 SecurityConfig에서 AuthenticationManager를 Bean으로 등록하고, LoginFilter를 생성할 때 주입해줘야 합니다.
  • addFilterAt(): 이 메소드가 핵심입니다. UsernamePasswordAuthenticationFilter가 동작해야 할 자리에 우리가 만든 new LoginFilter()를 대신 끼워 넣는 역할을 합니다.
  • sessionCreationPolicy(SessionCreationPolicy.STATELESS): JWT를 사용할 것이므로, 서버는 세션을 생성하거나 사용하지 않는 stateless 상태로 설정해야 합니다.

다음 단계

이제 로그인 요청을 가로채서 처리할 필터의 뼈대는 완성되었습니다. 하지만 아직 실제 DB와 연동하여 ID/PW를 검증하는 로직과 로그인 성공 시 JWT를 발급하는 로직이 비어있습니다.

다음 포스팅에서는 DB 기반의 회원 검증 로직을 구현하고 성공 핸들러에서 실제 JWT를 발급하고 검증하는 클래스를 만들어 보겠습니다.