[Backend] CORS Preflight 에러

주재완·2024년 12월 5일
0

Spring

목록 보기
4/4
post-thumbnail

CORS가 뭐길래

CORS(Cross-Origin Resource Sharing)는 서로 다른 출처를 가진 리소스의 공유를 허락하는 정책입니다. 출처가 다르더라도 요청과 응답을 주고받을 수 있도록 서버에 리소스 호출이 허용된 출처(Origin)을 명시해 주는 방식으로 허용하고 있습니다.

  • 출처(Origin)에는 프로토콜, 도메인, 포트 등이 있습니다.

CORS에러는 이러한 출처를 명시하지 않고 SOP(Same Origin Policy)를 미준수 할 때 발생합니다. 일반적인 해결하는 방법은 다음과 같습니다.

  • 서버에서 Access-Control-Allow-Origin 헤더를 설정해서 요청을 수락할 출처를 명시적으로 지정할 수 있습니다.
  • 프록시 서버를 활용하여 웹 애플리케이션에서 리소스로의 요청을 전달하는 방법이 있습니다.

그래서 첫번째 방식으로 아래와 같이 백엔드에서 allowedOrigins("*")를 작성하면서 모든 출처에 대해서 허용하는 방식으로 했습니다만...?

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry
			.addMapping("/**")
			.allowedOrigins("*") // 모든 출처 허용
			.allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.PUT.name(),
						HttpMethod.DELETE.name(), HttpMethod.HEAD.name(), HttpMethod.OPTIONS.name(),
						HttpMethod.PATCH.name())
			.maxAge(3600); // Pre-flight Caching
	}

???????
CORS 에러
가 되는 원인을 설명하고 이를 해결해보겠습니다.

CORS 동작 알아보기

axios() 요청을 할 시에 브라우저 - 서버 사이에서는 다음과 같은 과정을 거칩니다.

  1. 브라우저가 서버로 OPTIONS 메소드로 다음과 같은 예비 요청(Preflight)을 보냅니다.

    • 본인의 출처(origin)
    • 사용할 메소드(access-control-request-method)
    • 사용할 헤더(access-control-request-headers)
  2. 해당 예비 요청에 대한 다음과 같은 응답을 받습니다.

    • 서버에서 허용한 헤더(access-control-allow-headers)
    • 서버에서 허용한 메소드(access-control-allow-methods)
    • 서버에서 허용한 출처(access-control-allow-origin)
  3. 브라우저에서의 출처(origin)와 서버에서 허용한 출처(access-control-allow-origin)를 비교합니다.

    • 이 때 문제가 있으면 CORS 에러 발생

이상한 점

그런데... 1번 과정에서 이상한 점이 있습니다. 해당 요청을 한 axios를 보겠습니다.

async function info(request, success, fail) {
  // console.log("member.js 회원 정보 요청 ", request);
  await server
    .get("/member/info", {
      headers: {
        Authorization: `Bearer ${request.trim()}`
      }
    })
    .then(success)
    .catch(fail);
}

request를 컨솔에 찍어서 확인해보면 브라우저에서 저장하고 있는 JWT의 accessToken 이 나오게 됩니다. 그런데 Preflight에서는 해당 정보를 아무리 찾아봐도 안보입니다.

그리고 그러고 보니 info 요청이 2개가 와있는 것이 보입니다. Preflight와 본요청인데, 본요청이 윗줄에 해당합니다.

본 요청에서는 JWT가 보내지는 것을 확인할 수 있는데, 여기서 문제가 발생합니다.

바로, Preflight 요청에는 JWT 정보가 존재하지 않습니다. 즉, Preflight 역시 인증(authorization)이 필요한 요청으로 서버에서 받아 들여서 CORS 허용이 되지 않는 것입니다.

해결 방법

그러면 이를 어떻게 해결할까요? 다시 CORS 동작의 첫번째 과정을 봅니다.

  1. 브라우저가 서버로 OPTIONS 메소드로 다음과 같은 예비 요청(Preflight)을 보냅니다.

즉 Preflight 요청은 무조건 OPTIONS 메소드로 이루어집니다. 따라서 OPTIONS 메소드의 경우에는 별도의 인증 없이 모두 받아들이면 해결이 가능합니다.

우선, OPTIONS 메소드를 허용합니다. 사실 이는 전에 잘 적어두어서 추가적인 수정은 없었습니다.

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry
			.addMapping("/**")
			.allowedOrigins("*") // 모든 출처 허용
			.allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.PUT.name(),
						HttpMethod.DELETE.name(), HttpMethod.HEAD.name(), HttpMethod.OPTIONS.name(),
						HttpMethod.PATCH.name())
			.maxAge(3600);
	}

다음으로, 인증 확인을 위해 Interceptor를 확인합니다. 여기서, 메소드가 OPTIONS일 경우에는 별도의 인증을 받지 않도록 합니다.

@Slf4j
@Component
@RequiredArgsConstructor
public class AuthorizationInterceptor implements HandlerInterceptor {

	private final JWTUtil jwtUtil;

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		String authorizationHeader = request.getHeader("Authorization");
		if (request.getMethod().equals("OPTIONS")) {
			return true; // OPTIONS 메소드, 즉 Preflight 요청의 경우 추가 확인을 진행하지 않음
		}
		if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
			log.info("Missing or invalid Authorization header {}", authorizationHeader);
			throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing or invalid Authorization header");
		}

		String token = authorizationHeader.substring(7);

		if (!jwtUtil.checkToken(token)) {
			log.info("Token is expired or invalid {}", token);
			throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Token is expired or invalid");
		}

		String userId = jwtUtil.getUserId(token);
		request.setAttribute("userId", userId);
		return true;
	}
}

느낀점

이전에는 프론트엔드 프레임워크 대신 JSP로 모두 다 짜다보니 Frontend 서버와 Backend 서버가 따로 돌아가는 상상을 잘하지 못한 것 같고, 실제 이 두 서버가 요청과 응답을 주고 받기 위해서는 어떤 과정을 거치는지 모르고 있었습니다.

하지만, 실제 프론트엔드 프레임워크 Vuejs 를 사용하면서, 이 둘의 연결에 대해 고민해본 좋은 계기였습니다.

profile
언제나 탐구하고 공부하는 개발자, 주재완입니다.

0개의 댓글

관련 채용 정보