lty's Blog

JWT 헤더 검증과 CORS 본문

기타

JWT 헤더 검증과 CORS

LIMTAEYANG 2025. 5. 25. 17:18

1) JWT Authorization 헤더 값 전달

 

백엔드 서버에서 클라이언트의 요청이 유효한 요청인지 판별하기 위해 클라이언트에선 HTTP 헤더에 Authorization 값에 JWT 키값을 설정하여 백엔드 서버에 요청

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...

 

*  Bearer 문자열을 붙이는 것이 기술적으로는 필요 없지만 JWT 표준 준수, 호환성, 보안 필터 연동 위해 붙이는 것이 좋음(주로 요청이 잦은 액세스 토큰에만 사용)

 

2) JWT Filter에서 요청을 검증

 

한 번의 요청만 실행하는 OncePerRequestFilter 클래스를 상속받아 로직 작성

 

*  JWT 검증을 인터셉터에서 사용하지 않는 이유

 - 인증/인가는 가장 먼저 처리되어야 하며, 인터셉터는 그보다 나중에 작동하기 때문에 보안에 취약할 수 있어 필터에서 처리 

 

3) Prefilight 요청 Filter에서 처리

 

JWT 토큰을 헤더(Authorization)에 넣어 백엔드 서버에 요청 시 Prefilight 요청이 발생하여 CORS에 걸리지 않게 하려면 OTPIONS 요청도 허용을 해줘야 한다.

 

* 참고 Prefilight 요청 조건 : CORS와 브라우저 prefilight 요청

 

CORS 와 브라우저 prefilight 요청

* CORS(Cross-Origin Resource Sharing) 란 브라우저에서 서로 다른 출처(origin) 간의 자원 요청을 제어하는 보안 정책* Origin 은 프로토콜, 호스트, 포트가 모두 같아야 같은 Origin으로 인식하고 다른 출처에

sunhistory.tistory.com

 

Spring WebMvcConfigurer CORS 설정으로 적용 시 필터보다 뒷단에 있어 CORS 설정이 적용되지 않는다. 

*  Filter에 도달 후 Dispatcher Servlet에 도달

@Configuration
@Description(description = "인터셉터 등 기본 웹 설정을 위한 클래스")
public class WebConfig implements WebMvcConfigurer {

    @Value("${allow.client.domain}")
    String allowDomain;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       .....
    }

	/* CORS 설정 */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins(allowDomain) // CORS 허용 도메인 
                .allowedMethods("GET", "POST", "OPTIONS") // 허용할 메서드 
                .allowedHeaders("*")	// 허용할 헤더 설정
                .allowCredentials(true); // 쿠키나 보안적인 값을 전달 할지 여부 
    }

}

 

 

* Filter에서 JWT를 처리할 경우 JWT에서 OPTIONS 요청 처리를 해야 한다. 

 

JWT FILTER 

@Component
@RequiredArgsConstructor
public class JwtCheckFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    @Value("${allow.client.domain}")
    String allowDomain;

    @Override // 필터제외 설정 
    protected boolean shouldNotFilter(HttpServletRequest request){
        return false;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        String headerStr = request.getHeader("Authorization");

        /* preflight 요청 CORS 방지 */
        response.setHeader("Access-Control-Allow-Origin", allowDomain);
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
        response.setHeader("Access-Control-Allow-Credentials", "true");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }

        if(headerStr == null || !headerStr.startsWith("Bearer ")){
            handle403Exception(response, "Not Found Token");
            LoggingService.info(getClass(), "Not Found Token :" + headerStr);
            return;
        }
        
        ...................
    }

    private void handle403Exception(HttpServletResponse response, String error) throws IOException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json");
        response.getWriter().println("{\"error\": \"" + error+"\"}");
    }

    private void handle401Exception(HttpServletResponse response, String error) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        response.getWriter().println("{\"error\": \"" + error+"\"}");
    }
}

 

Comments