프론트엔드와 연동해서 데모하는 과정 중 인가가 필요한 API 호출을 하면 백엔드 서버에 예외가 발생했다.
브라우저가 보낸 예비 요청(OPTIONS 메서드)에는 토큰값이 담기지 않기 때문에 인가 인터셉터의 토큰 검증 과정(preHandle 메서드)에서 예외가 발생한 것이다.
인가 인터셉터에서 OPTIONS 메서드로 요청이 오면 토큰을 검사하지 않도록 했다.
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler)
throws Exception
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
return true;
}
final String token = AuthorizationExtractor.extract(request);
validateExpiredToken(token);
return true;
}
먼저 CORS 란 교차 출처 리소스 공유로 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에서 알려주는 체제이다.(출처를 비교할 때 프로토콜, 호스트, 포트 세 가지를 비교한다)
위에 사진처럼 실제 요청을 받은 서버와 요청이 시작된 서버가 다르다. 이 경우가 교차 출처 리소스를 요청한 것이다. 이때 백엔드 서버에서 교차 출처 리소스 공유 요청을 허용하겠다는 응답을 해줘야 클라이언트는 브라우저를 통해 응답 페이지를 볼 수 있다. 만약 허용하지 않는다면 서버에서 정상적인 응답을 해줘도 브라우저는 CORS 허용 에러 메세지를 띄운다.
아래 코드와 같이 설정해서 CORS 허용 응답을 할 수 있다. 아래 코드처럼 모든 출처에 대해서 허용할 수 있지만 이것보다는 계약된 서버의 출처만 허용하도록 해야 한다.(이유는 아래의 CORS 동작 방식 중 Credentialed requests 참고)
@Configuration
public class WebConfig implements WebMvcConfigurer {
public static final String ALLOWED_METHOD_NAMES = "GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,PATCH";
@Override
public void addCorsMappings(final CorsRegistry registry) {
// 허용할 출처(Origin) 을 따로 명시하지 않았기 때문에 모든 출처(Origin) 를 허용한다
registry.addMapping("/**")
.allowedMethods(ALLOWED_METHOD_NAMES.split(","))
.exposedHeaders(HttpHeaders.LOCATION);
}
}
모든 출처에 대한 허용 설정을 하면 아래와 같이 응답받게 된다.
그렇다면 브라우저는 왜 CORS 를 만들었을까?
사용자가 악성 웹사이트에 접속한 경우 브라우저는 출처를 비교해서 사용자가 접속한 웹사이트(악성 웹 사이트)가 실제 요청을 받는 서버(백엔드 서버)와 계약(백엔드 서버에서 허용한 출처에 포함 여부)되지 않은 경우 본 요청을 보내지 않아 CSRF 를 예방할 수 있기 때문이다.
CORS 가 동작하는 방식은 세 가지로 나눌 수 있다.
Simple requests
단순 요청은 예비 요청을 보내지 않고 바로 서버에 본 요청을 한다. 만약 조건이 아래와 같이 까다롭지 않으면 CSRF 에 그대로 노출될 것이다.
GET, HEAD, POST 중 하나의 메서드일 것.
유저 에이전트가 자동으로 설정한 헤더외에, 수동으로 설정할 수 있는 헤더는 Fetch 명세에서 “CORS-safelisted request-header”로 정의한 헤더 뿐이다.
Accept
Accept-Language
Content-Language
Content-Type
Content-Type 헤더는 다음의 값들만 허용한다.
application/x-www-form-urlencoded
multipart/form-data
text
*POST 메서드의 경우 대부분 Content-Type 이 application/json이기 때문에 단순 요청이 아닌 예비 요청을 보낸다.
Preflighted request
예비 요청은 브라우저가 먼저 본 요청을 보내기 전 안전한 요청인지 확인한다. 백엔드 서버는 예비 요청을 먼저 받음으로써 알 수 없는 출처의 본 요청을 받지 않을 수 있다. 예비 요청에 대한 응답을 확인해서 브라우저는 본 요청 여부를 정한다.
(POST 요청이지만 Content-Type이 application/json 이기 때문에 예비 요청을 한다.)
(예비 요청에 대한 서버 응답의 헤더)
Credentialed requests
인증정보를 포함한 요청을 하게 되면 브라우저에서는 응답 헤더에서 두 가지를 확인한다.
두 조건을 만족하면 브라우저는 응답 페이지를 띄어준다.
Credentialed requests 는 단순 요청과 예비 요청에서 함께 처리된다. 즉 어떤 요청이든 인증 정보가 담길 경우 브라우저는 더 많은 조건을 검사해서 사용자에게 보안의 이점을 얻도록 해준다.
백엔드 서버에서 예비 요청에 대한 캐시 설정(Access-Control-Max-Age)
참고
https://developer.mozilla.org/ko/docs/Web/HTTP/CORS
https://prolog.techcourse.co.kr/studylogs/2414
최고예용 👍👍👍