SpringSecurity Filter를 사용하여 JWT 로그인을 어떤 식으로 구현하였는지 인증 아키텍처와 소스코드를 비교하면서 살펴보겠습니다.
SecurityConfiguration 구현
@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함(Security Configuration 지정)
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
private final AuthenticationConfiguration authenticationConfiguration;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return filter;
}
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() {
return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf((csrf) -> csrf.disable());
// 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
http.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
.requestMatchers("/").permitAll() // 메인 페이지 요청 허가
.requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
// 로그인 페이지
http.formLogin((formLogin) ->
formLogin
.loginPage("/api/user/login-page").permitAll()
);
// 필터 관리
http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
- @EnableWebSecurity 어노테이션은 스프링 시큐리티를 활성화하고 웹 보안 설정을 구성하는 데 사용
- addFilterBefore() 메소드를 사용해 필터 체인 순서를 결정
JwtAuthorizationFilter
@Slf4j(topic = "JWT 검증 및 인가")
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
public JwtAuthorizationFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
String tokenValue = jwtUtil.getJwtFromHeader(req);
if (StringUtils.hasText(tokenValue)) {
if (!jwtUtil.validateToken(tokenValue)) {
log.error("Token Error");
return;
}
Claims info = jwtUtil.getUserInfoFromToken(tokenValue);
try {
setAuthentication(info.getSubject());
} catch (Exception e) {
log.error(e.getMessage());
return;
}
}
filterChain.doFilter(req, res);
}
// 인증 처리
public void setAuthentication(String username) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = createAuthentication(username);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
// 인증 객체 생성
private Authentication createAuthentication(String username) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
- JwtAuthorizationFilter는 JWT 토큰을 검증하여 사용자 인증을 처리하고, 인증된 정보를 SecurityContextHolder에 저장하는 역할
- 보통 SpringSecurity 인증 흐름은 UsernamePasswordAuthenticationFilter가 사용자의 아이디와 비밀번호를 받아 인증을 수행하고 인증이 성공하면 AuthenticationManager와 SecurityContextHolder가 사용자 정보를 인증된 객체로 저장
- 그러나 SpringSecurity 기본 인증 방식과 다르게, 요청에 포함된 JWT 토큰을 검증하여, 사용자 세션을 유지할 필요 없이 인증된 사용자 정보를 UsernamePasswordAuthenticationToken을 생성하여 SecurityContextHolder에 저장(AuthenticationManager 사용 X)
JwtAuthenticationFilter
@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
setFilterProcessesUrl("/api/user/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
requestDto.getUsername(),
requestDto.getPassword(),
null
)
);
} catch (IOException e) {
log.error(e.getMessage());
throw new RuntimeException(e.getMessage());
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();
String token = jwtUtil.createToken(username, role);
response.addHeader(JwtUtil.AUTHORIZATION_HEADER, token);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
response.setStatus(401);
}
}
- JwtAuthenticationFilter는 로그인 요청을 처리하고, 인증 성공 시 JWT 토큰을 생성하여 클라이언트에 반환하는 역할을 하는 필터
- UsernamePasswordAuthenticationFilter를 상속받아 인증을 시도합니다.
- AuthenticationManager에 UsernamePasswordAuthenticationToken을 전달하여 인증을 수행
- 인증이 성공하였을 때 사용자 정보를 SecurityContextHolder에 저장하지 않고 JWT만 생성하여 응답 헤더에 포함하여 전송
결론
- JwtAuthenticationFilter에서는 직접적으로 SecurityContextHolder를 사용하지 않음. 대신, AuthenticationManager를 통해 인증이 성공한 후 JWT를 생성해 클라이언트에게 반환하는 방식
- SpringSecurity의 기본 세션 기반 인증에서는 SecurityContextHolder에 인증 정보를 저장하여 세션을 통해 인증 상태를 유지. 그러나, JWT 기반 인증에서는 각 요청마다 JWT가 포함되어 전달되므로 SecurityContextHolder에 인증 정보를 저장할 필요가 없음
- JwtAuthorizationFilter는 JWT를 검증하고 사용자 정보를 SecurityContextHolder에 설정하여 요청 단위의 인증 상태를 유지
'Spring' 카테고리의 다른 글
[Spring] IoC(제어의 역전), DI(의존성 주입) 이해하기 (0) | 2024.11.17 |
---|---|
[Spring] 생성(Creational) 패턴 - 추상 팩토리 (0) | 2024.11.13 |
[Spring] JWT(Json Web Token) (0) | 2024.11.11 |
[Spring] Servlet Filter 란? (1) | 2024.11.10 |
[Spring] SpringSecurity - Filter Chain (0) | 2024.11.10 |