ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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()
                }

     

     

Designed by Tistory.