-
[SpringSecurity] 도메인이 다를경우, antMatchers 권한 처리 방법.Spring 2022. 3. 15. 00:18
통합배포가 아닌 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() }
'Spring' 카테고리의 다른 글
[SpringSecurity] OAuth2.0 ... Authorization Code Grant 인증 (0) 2021.12.31 [SpringSecurity] OAuth 개념 정리 (0) 2021.12.30 [Spring] 스프링 빈은 무조건 Thread-safe 할까? (0) 2021.12.23