비로그인 사용자의 권한 처리
서비스를 개발하면서 비로그인 사용자의 권한 처리가 필요하게 되었다.
비로그인 사용자도 글 조회는 가능하지만 글 작성, 수정, 삭제는 불가능하게 만들어야했다.
기존의 JWT Filter에서는 비로그인 사용자의 경우 필터단에서 바로 400번대 오류를 반환했기 때문에 수정이 필요했다.
어떻게 구현할까 고민하다가 스프링 시큐리티의 기능인 메서드 시큐리티를 적용하여 구현하기로 하였다.
구현하기
구현 순서는 아래와 같다.
- JWT Filter 및 SecurityConfig 수정
- MethodSecurityConfig 추가
- 메서드 시큐리티 적용
1. JWT Filter 및 SecurityConfig 수정
1-1. JWT Filter 수정
우선 기존의 JWT Filter 로직을 변경해야 했다.
기존에는 JWT 인증이 성공했을 때만 인증된 유저 정보를 시큐리티 컨텍스트에 담아 다음 필터로 넘겼었다.
실패하면 바로 필터단에서 HTTP Response 를 반환하였다.
수정 후에는 JWT 인증이 성공하면 인증된 유저 정보를, 실패하면 GUEST 유저를 시큐리티 컨텍스트에 담아서 다음 필터로 넘긴다.
성공하든 실패하든 다음 필터를 거치는 방식이다.
JwtAuthFilter
@Slf4j
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
public JwtAuthFilter(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Cookie[] cookies = request.getCookies();
log.info("[JWT 필터 입장]");
try {
String jwt = jwtUtils.getJwtFromCookie(cookies);
Authentication authentication = jwtUtils.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
} catch (AuthenticationException e) {
User guest = User.builder()
.id(0L)
.email("guest")
.password("guest")
.build();
Authentication authentication = new AnonymousAuthenticationToken(
"guest",
new UserPrincipal(guest),
List.of(
new SimpleGrantedAuthority("ROLE_GUEST")
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
}
1-2. SecurityConfig수정
JWT 필터 로직이 수정됐으므로 JWT Filter를 빈 등록 할 때 ExcludeUrlsRequestMatcher가 필요 없어져서 제거하였다. 또한 SecurityConfig의 SecurityFilterChain을 모든 요청에 대해 허용해두었다.만약 인증이 필요한 URL이 있다면 추가하면 된다.
SecurityConfig
@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@Profile("!test")
public class SecurityConfig {
private final ObjectMapper objectMapper;
private final UserDetailsService userDetailsService;
private final JwtUtils jwtUtils;
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring()
.requestMatchers("/favicon.ico")
.requestMatchers("/error")
.requestMatchers(toH2Console()); // TODO: 배포 시 제거
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(authorize -> authorize
.anyRequest().permitAll()
)
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jsonAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(e -> {
e.authenticationEntryPoint(new Http401Handler(objectMapper));
e.accessDeniedHandler(new Http403Handler(objectMapper));
})
.csrf(AbstractHttpConfigurer::disable)
.build();
}
@Bean
public JwtAuthFilter jwtAuthFilter() {
return new JwtAuthFilter(jwtUtils);
}
@Bean
public JsonAuthFilter jsonAuthFilter() {
JsonAuthFilter filter = new JsonAuthFilter("/api/auth/login", objectMapper);
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(new LoginSuccessHandler(objectMapper, jwtUtils));
filter.setAuthenticationFailureHandler(new LoginFailHandler(objectMapper));
return filter;
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(provider);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new SCryptPasswordEncoder(
16,
8,
1,
32,
64
);
}
}
2. MethodSecurityConfig 추가
스프링 시큐리티의 메서드 시큐리티 기능을 쓰려면 @EnableMethodSecurity 애노테이션을 달아줘야 한다
SecurityConfig에서 추가해도 되지만 따로 Config 파일로 뺐다.
추후 Permission Evaluator를 추가하기에도 이 쪽이 관리하기 편할거라고 생각했다.
MethodSecurityConfig
@Configuration
@EnableMethodSecurity
@RequiredArgsConstructor
public class MethodSecurityConfig {
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
return handler;
}
}
3. 메서드 시큐리티 사용
메서드 시큐리티는 Spring EL(Spring Expression Language)을 사용한다.
아래처럼 @PreAuthorize 애노테이션과 문자열을 입력하는 방식이다.
"hasRole('ROLE_USER')" 를 입력하면 ROLE_USER 인 유저만 사용가능하다.
비로그인 유저는 ROLE_GUEST 로 설정해두었기 때문에 시큐리티에서 걸러지게 된다.

단순 ROLE 뿐 아니라 추가 필터링 필요 시 Permission Evaluator 를 사용할 수도 있지만 이건 다음에 다뤄보도록 하겠다.
'백엔드 > Spring Security' 카테고리의 다른 글
시큐리티 환경에서 테스트 2: 컨트롤러 테스트하기 (0) | 2024.04.25 |
---|---|
시큐리티 환경에서 테스트 1: @WithMockUser 커스텀 (0) | 2024.04.24 |
JwtFilter 만들기 (0) | 2024.04.16 |