Spring

[SpringSecurity] 도메인이 다를경우, antMatchers 권한 처리 방법.

찐팡민 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()
            }