Spring Security + JWT로 로그인 구현하기 (1) - 커스텀 필터 만들기
이번에는 세션 방식에서 JWT를 사용한 인증 방식을 구현해 볼 예정입니다. 이를 위해 SecurityConfig에서 formLogin을 비활성화했는데 이렇게 하면 기존의 로그인 처리를 담당하던 UsernamePasswordAuthenticationFilter가 동작하지 않게 됩니다.
따라서 우리는 JWT 방식에 맞는 새로운 로그인 처리 필터를 직접 만들고 등록해야 합니다.
목표
- ID, PW 검증을 위한 커스텀 필터 작성
- DB 회원 정보를 기반으로 한 검증 로직 작성
- 로그인 성공 시 JWT를 반환할 SuccessHandler 구현
- 작성한 커스텀 필터를 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(): 로그인 요청을 가로채는 가장 핵심적인 메소드입니다.
- 사용자가 입력한 username과 password를 추출합니다.
- 인증을 진행해 줄 AuthenticationManager에게 넘겨주기 위해 UsernamePasswordAuthenticationToken 객체를 생성합니다.
- 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를 발급하고 검증하는 클래스를 만들어 보겠습니다.
'백엔드 스터디' 카테고리의 다른 글
| Swagger로 API 자동 관리하기 (1) | 2025.08.24 |
|---|---|
| 스프링 OAuth2 로그인 구현하기 4 - JWT (2) | 2025.08.17 |
| 스프링 OAuth2 로그인 구현하기 2 (4) | 2025.08.09 |
| 스프링 OAuth2 로그인 구현하기 1 (2) | 2025.08.04 |
| 디스패처 서블릿이란? (0) | 2025.04.02 |