[SpringSecurity] 도메인이 다를경우, antMatchers 권한 처리 방법.
통합배포가 아닌 Frontend와 Backend서버를 따로 배포한다면 위와같이 CORS 에러가 발생하는 것을 자주 볼 수 있습니다. 위와 같은 에러 메시지는 가장 기본적인 cors에러의 하나입니다.
SecurityConfig , WebMvcConfig , ResponseMessage 여러 단에서 처리 할 수 있습니다!
대표적인 풀이
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowCredentials(true)
.allowedHeaders("*")
.allowedMethods("*")
.exposedHeaders(JwtTokenUtil.HEADER_STRING)
.maxAge(3600L);
}
}
오늘 살펴볼 문제는 이 cors에러와 더불어 권한을 처리할 때 SpringSecurity에서는 추가로 어떤 에러가 발생하고 해결책에 대해서 알아보겠습니다.
ex ) 권한 관련 접근 예시 코드
.access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
.antMatchers("/manager/**")
SpringSecurity에서는 타 도메인에 대한 권한 관련 접근 시 PreFlight이라는 과정을 먼저 수행합니다.
PreFilght이란?
Preflight Request
- 실제 요청을 보내도 안전한지 판단하기 위해 preflight 요청을 먼저 보냅니다.
- Preflight Request는 actual 요청 전에 인증 헤더를 전송하여 서버의 허용 여부를 미리 체크하는 요청입니다.
- 이 요청으로 트래픽이 증가할 수 있는데 서버의 헤더 설정으로 캐쉬가 가능합니다.
- 브라우저에서는 다른 도메인으로 보내게 될 때 해당 도메인에서 CORS 를 허용하는지 알아보기 위해 preflight 요청을 보내는데 이에대한 처리가 필요합니다.
- preflight 요청은 OPTIONS 메서드를 사용하며 "Access-Control-Request-*" 형태의 헤더로 전송합니다.
타 도메인에 요청을 보낼 시, 해당 도메인에서 Preflight 을 설정하지 않으면 다음과 같은 에러문을 볼 수 있습니다.
'http://localhost:8000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.
우선적으로 제 코드를 예시로 전체적인 플로우가 어떻게 구성되고, 해결책이 무엇인지 알아보겠습니다.
1. authentication 객체에 권한 정보를 추가하는 BasicAuthenticationFilter를 add합니다 .
2. 권한 정보는 Header에서 JWT 토큰을 들고와서 UsernamePasswordAuthenticationToken 에 해당 정보를 주입하여 authentication으로 넘겨줍니다.
3. authentication 정보를 기반으로 권한(antMatchers --- hasRole)을 식별합니다.
JwtAuthenticationFilter ..extends BasicAuthenticationFilter 코드
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
private UserService userService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, UserService userService) {
super(authenticationManager);
this.userService = userService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Read the Authorization header, where the JWT Token should be
String header = request.getHeader(JwtTokenUtil.HEADER_STRING);
System.out.println(header);
// If header does not contain BEARER or is null delegate to Spring impl and exit
if (header == null || !header.startsWith(JwtTokenUtil.TOKEN_PREFIX)) {
filterChain.doFilter(request, response);
return;
}
try {
// If header is present, try grab user principal from database and perform authorization
Authentication authentication = getAuthentication(request);
// jwt 토큰으로 부터 획득한 인증 정보(authentication) 설정.
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception ex) {
ResponseBodyWriteUtil.sendError(request, response, ex);
return;
}
filterChain.doFilter(request, response);
}
@Transactional(readOnly = true)
public Authentication getAuthentication(HttpServletRequest request) throws Exception {
String token = request.getHeader(JwtTokenUtil.HEADER_STRING);
// 요청 헤더에 Authorization 키값에 jwt 토큰이 포함된 경우에만, 토큰 검증 및 인증 처리 로직 실행.
if (token != null) {
// parse the token and validate it (decode)
JWTVerifier verifier = JwtTokenUtil.getVerifier();
JwtTokenUtil.handleError(token);
DecodedJWT decodedJWT = verifier.verify(token.replace(JwtTokenUtil.TOKEN_PREFIX, ""));
String userId = decodedJWT.getSubject();
// Search in the DB if we find the user by token subject (username)
// If so, then grab user details and create spring auth token using username, pass, authorities/roles
if (userId != null) {
// jwt 토큰에 포함된 계정 정보(userId) 통해 실제 디비에 해당 정보의 계정이 있는지 조회.
User user = userService.getUserByUserId(userId);
if(user != null) {
// 식별된 정상 유저인 경우, 요청 context 내에서 참조 가능한 인증 정보(jwtAuthentication) 생성.
SsafyUserDetails userDetails = new SsafyUserDetails(user);
Collection<? extends GrantedAuthority> test = userDetails.getAuthorities();
System.out.println(test.stream().toArray().toString());
UsernamePasswordAuthenticationToken jwtAuthentication = new UsernamePasswordAuthenticationToken(user.getName(),null,userDetails.getAuthorities());
// jwtAuthentication.setDetails(userDetails);
return jwtAuthentication;
}
}
return null;
}
return null;
}
}
하지만 결과는...?
'http://localhost:8000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.
이를 해결하기위해서 엄청난 삽질을 했지만 ... ㅠㅠ
생각보다 간단한 방법으로 해결할 수 있었습니다!
SpringSecurity에서 한 줄 코드로 해결하였습니다.
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 기반 인증이므로 세션 사용 하지않음
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager(), userService)) //HTTP 요청에 JWT 토큰 인증 필터를 거치도록 필터를 추가
.authorizeRequests()
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
.antMatchers("/user/**")
.access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
.antMatchers("/manager/**")
.access("hasRole('ROLE_MANAGER')")
.antMatchers("/admin/**")
.access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
}