0. 로그인 과정
1. 로그인 요청 (POST)
2. DelegatingFilterProxy가 요청을 가로챔
3. SecurityFilterChain 내부 여러 필터 중, UsernamePasswordAuthenticationFilter가 실행되어 요청을 받음.
4. AuthenticationManager가 인증을 시도, UserDetailService에서 사용자 정보를 조회
5. 인증 성공하면 SecurityContext에 사용자 정보를 저장하고, JWT를 발급해 응답.
* 용어 설명
Delegating Filter Proxy : 건물 입구 보안 게이트.
- 서블릿 컨테이너(Tomcat)에 등록되는 첫 번재 필터.
- 들어오는 모든 요청을 Spring Security 필터 체인(SecurityFilterChain) 각 검문소로 전달.
SecurityFilterChain : 보안 게이트 내부의 여러 개의 보안 검문소가 모인 곳
- 여러 보안 검문소가 모인 곳
- Spring Security에서 여러 개의 보안 필터가 순차적으로 실행되는 체인
- 로그인 요청, JWT 인증, CSRF 보호 등 여러 보안 검사를 담당하는 필터들을 포함
- 필터 종류 : UsernamePasswordAuthenticationFilter(아이디/비번 로그인 검사),
BasicAuthenticationFilter(HTTP Basic 인증 검사),
BearerTokenAuthenticationFilter(JWT 토큰 검사)
UsernamePasswordAuthenticationFilter : 신분증과 비밀번호 확인하는 보안요원
- /login 요청을 가로채서 아이디(username)와 비밀번호(password)를 확인
- AuthenticationManager 에게 검증을 요청
AuthenticationManager : 보안요원이 확인한 정보를 최종 검토하는 보안 관리 부서
- 검증을 직접 하지는 않고, UserDetailService를 통해 DB에서 사용자 정보를 조회함.
UserDetailService : 회사의 사원 정보 조회 시스템
- DB에서 username을 기반으로 사용자 정보를 조회
- JWT방식에서는 CustomUserDetailService로 직접 구현.
- loadUserByUsername 메서드.
1. LoginFilter 구현
JWT 기반 인증을 사용하기 때문에, 이전에 formLogin()을 비활성화했다.
따라서 UsernamePasswordAuthenticationFilter 대신 우리가 직접 커스텀한 로그인 필터를 구현해야 한다.
여기서 username과 password를 추출하고,
UsernamePasswordAuthenticationToken을 생성하여 AuthenticationManager에 인증 요청을 보낸다.
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 {
// 클라이언트 요청에서 username과 password 추출
String username = obtainUsername(request);
String password = obtainPassword(request);
// 인증을 위해 스프링 시큐리티의 인증 토큰 생성
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
// AuthenticationManager에게 토큰을 넘겨 인증을 요청
return authenticationManager.authenticate(authToken);
}
// 로그인 성공 시 실행
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException {
}
// 실패시 실행
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
}
}
2. SecurityConfig에 로그인필터 등록
// AuthenticationManager Bean 등록
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf((auth) -> auth.disable())
.formLogin((auth) -> auth.disable())
.httpBasic((auth) -> auth.disable())
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login", "/", "/join").permitAll()
.anyRequest().authenticated())
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 커스텀한 필터를 등록
// 생성자 파라미터인 authenticationManager를 추가 등록해줘야 에러 안남.
http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);
return http.build();
}