Lv.9 Spring security 도입하기
spring security와 jwt를 통합해 인증 로직을 표준화하는 과제를 받았다.
해당 과정을 정리해보도록 하자.
1. 기존 인증 구조 살펴보기
JwtFilter: Servlet filter를 이용해 jwt토큰을 검증하고 요청값에 사용자 정보를 설정하는 역할
spring security와 독립적으로 동작한다. jwt검증과 관련된 로직을 직접 구현하고 있고 spring security의 securityContextHolder를 사용하지 않고 요청값에 데이터를 설정하고 있다.
FilterConfig: FilterRegistrationBean으로 필터를 등록하고 url 패턴에 따라 요청을 처리하는 역할
spring security는 자체 필터 체인을 관리하므로 별도의 filterconfig는 필요하지 않다. 인증 로직이 spring security의 인증 흐름과 분리되어 있다.
AuthUserArgumentResolver: 컨트롤러클래스의 메서드에 사용자 정보를 주입하는 역할
spring security의 securityContextHolder를 사용하면, 전역 인증 정보를 관리할 수 있음에도 요청마다 별도의 인증 정보를 생성하고 있다.
따라서, spring security의 표준 인증 체계를 활용하지 않아서 중복 코드가 발생하고 있다.
securityFilterChain과 authenticationProvider를 활용해 인증-인가 로직을 표준화하고,
각종 filter를 spring security filter로 통합하고, securityContextHolder를 활용하여 인증 정보를 전역적으로 관리해보자.
2. 통합 과정 - JwtAuthenticationFilter
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = header.substring(7);
Claims claims = jwtUtil.extractClaims(token);
if (claims != null) {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(token, null, null)
);
}
filterChain.doFilter(request, response);
}
}
spring security의 oncePerRequestFilter를 상속하여 jwt인증 필터를 구현하였다.
OncePerRequestFilter란, "한 요청에 대해 중복되지 않게 필터를 실행"한다. 필터 체인에서 동일한 요청이 여러 필터체인을 거치는 경우 중복 실행을 방지시켜준다.
요청의 속성에 이 필터가 이미 실행되었는지를 기록하고, 동일 요청에 대해 다시 실행되지 않도록 보장한다.
내부에서 setAttribute("이미 실행된 필터", Boolean.TRUE); 를 통해, HTTP 요청 단위로 중복 실행되지 않도록 보장해준다.
jwt토큰을 헤더에서 추출하고 유효성검사를 한 뒤, securityContextHolder에 인증 정보를 저장하는 로직을 구성했다.
2. 통합 과정 - JwtAuthenticationProvider
@Component
@RequiredArgsConstructor
public class JwtAuthenticationProvider implements AuthenticationProvider {
private final JwtUtil jwtUtil;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String token = (String) authentication.getCredentials();
Claims claims = jwtUtil.extractClaims(token);
if (claims == null) {
throw new RuntimeException("Invalid JWT token");
}
Long userId = Long.parseLong(claims.getSubject());
String email = claims.get("email", String.class);
UserRole userRole = UserRole.valueOf(claims.get("userRole", String.class));
String nickname = claims.get("nickname", String.class);
AuthUser authUser = new AuthUser(userId, email, userRole, nickname);
UserDetails userDetails = User.builder()
.username(authUser.email())
.password("")
.roles(authUser.userRole().name())
.build();
return new UsernamePasswordAuthenticationToken(
userDetails, token, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
spring security의 authenticationProvider를 구현해 jwt의 유효성을 검증하고 인증 객체를 생성했다.
AuthenticationProviderㄹ는 spring security에서 인증 요청을 처리하는 핵심 컴포넌트이다.
인증 로직 처리(사용자 이름/비번 확인, jwt토큰 유효성 검증),
인증 객체 생성(인증 성공 시 Authentication 객체 반환, securityContextHolder에 저장돼 app전반에서 사용 가능),
처리 가능한 인증 타입을 확인(supports 메서드)
등의 역할을 가진다.
비밀번호는 사용하지 않으므로 ""로 처리했다. 현재 당장에 비밀번호를 사용하지 않기에 삭제해도 되지만, 다른 인증 방식이 추가될 경우를 고려해 구조를 유지했다.
3. 통합 과정 - SecurityConfig
@Configuration
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final JwtAuthenticationProvider jwtAuthenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.authenticationProvider(jwtAuthenticationProvider)
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config
) throws Exception {
return config.getAuthenticationManager();
}
}
spring security의 securityFilterChain을 설정해서 요청별 인증-인가를 관리한다.
jwtAuthenticationFilter를 체인에 추가하고, jwtAuthenticationProvider를 설정했다.
/auth요청은 permitAll을 통해 열어두고, /admin은 ADMIN role을 가지고 있어야만 접근 가능하게 구현했다.
4. 통합 결과
JwtFilter -> JwtAuthenticationFilter로 대체
FilterConfig -> spring security가 필터체인을 관리해서 불필요
AuthUserArgumentResolver -> 인증정보를 securityContextHolder에서 직접 가져오게 변경
spring security의 표준 인증/인가 체계를 사용해 코드 구조가 단순해지고, 유지보수하기 편해졌다.