교차 출처 리소스 공유 (Cross-Origin Resource Sharing)
- 추가 HTTP 헤더를 사용하여, 한 출처에서 실행중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제
- 웹 애플리케이션은 리소스가 자신의 출처와 다를 때 교차 출처 HTTP 요청을 실행한다.
Origin: https://velog.io
이후 서버가 요청에 대한 응답을 할 때 응답 헤더의 Access-Countrol-Allow-ORigin 이라는 값에 접근이 허용된 출처를 알려주고 응답을 받은 브라우저는 클라이언트가 보낸 요청의 Origin 과 서버가 보내준 응답을 비교하여 이 응답이 유효한 응답인지 아닌지 결정한다.
CORS 요청에는 단순 요청
과 실행 전 요청
두 가지 유형이 있다.
단순요청은 특정한 조건을 만족하는 경우에만 성립할 수 있다.
요청 메서드(Method)는 GET, HEAD, POST 중 하나여야 한다.
Content-Type 헤더가 application/x-www-form-urlencoded
, multipart/form-data
, text/plain
중 하나여야한다. 아닐 경우 예비 요청으로 동작된다.
Accept
, Accept-Language
, Content-Language
, Content-Type
, DPR, Downlink
, Save-Data
, Viewport-Width
, Width
헤더일 경우 에만 적용된다.
예를 들어, 브라우저에서 웹사이트 A에서 웹사이트 B로 데이터를 가져오려고 할 때, 위의 조건을 모두 만족하면 해당 요청은 "단순 요청"으로 간주되어 별도의 사전 요청(예비 요청)없이 바로 실행된다.
자바스크립트의 fetch API를 통해 브라우저에게 리소스를 받아오게 하면 브라우저는 서버로 예비요청을 먼저 보내고 서버는 이 예비요청에 대한 응답으로 어떤 것을 허용하고 어떤것을 금지하고 있는지에 대한 정보를 담아서 브라우저로 다시 보내준다.
이후 브라우저는 보낸 요청과 서버가 응답해준 정책을 비교하여 해당 요청이 안전한지 확인하고 본 요청을 보내게 된다. 이후 서버가 본 요청에 대한 응답을 하면 최종적으로 이 응답 데이터를 자바스립트로 넘겨준다.
다른 출처간 통신에서 좀 더 보안을 강화하고 싶을 때 사용한다.
기본적으로 제공하는 리소스 요청 API인 XMLHttpRequest 또는 fetch는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 함부로 요청에 담지 않는다. 이때 요청에 인증과 관련되 정보를 담을 수 있게 해주는 옵션이 credentials옵션이다.
옵션값 | 설명 |
---|---|
same-origin(기본값) | 같은 출처간 요청에만 인증 정보를 담을 수 있다 |
include | 모든 요청에 인증 정보를 담을 수 있다 |
omit | 모든 요청에 인증 정보를 담지 않는다 |
만약 same-origin 이나 include와 같은 옵션을 사용하여 리소스 요청에 인증 정보가 포함된다면, 브라우저는 다른 출처의 리소를 요청할 때 Access-Control-Allow-Origin만 확인하는 것이 아니라 다른 조건을 추가로 검사한다.
요청에 인증정보가 담겨있는 상태에서 다른 출처의 리소스를 요청하게 되면 브라우저는 CORS정책 위반 여부를 검사하는 룰에 다음 두가지를 추가하게 된다.
Access-Control-Allow-Origin 에는 모든 요청을 허용하는*을 사용할 수 없으며, 명시적인 URL이어야 한다
응답 헤더에는 반드시 Access-Control-Allow-Credentials:true가 존재해야 한다
서버 측에서 적절한 설정을 해주어야 한다.
서버에서 Access-Control-Allow-Origin 헤더에 허용할 출처를 기재해서 클라이언트에 응답하면 된다.
Access-Control-Allow-Origin: https://example.com
import javax.servlet.*;
public class CORSInterceptor implements Filter {
private static final String[] allowedOrigins = {
"http://localhost:3000", "http://localhost:5500", "http://localhost:5501",
"http://127.0.0.1:3000", "http://127.0.0.1:5500", "http://127.0.0.1:5501"
};
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String requestOrigin = request.getHeader("Origin");
if(isAllowedOrigin(requestOrigin)) {
// Authorize the origin, all headers, and all methods
((HttpServletResponse) servletResponse).addHeader("Access-Control-Allow-Origin", requestOrigin);
((HttpServletResponse) servletResponse).addHeader("Access-Control-Allow-Headers", "*");
((HttpServletResponse) servletResponse).addHeader("Access-Control-Allow-Methods",
"GET, OPTIONS, HEAD, PUT, POST, DELETE");
HttpServletResponse resp = (HttpServletResponse) servletResponse;
// CORS handshake (pre-flight request)
if (request.getMethod().equals("OPTIONS")) {
resp.setStatus(HttpServletResponse.SC_ACCEPTED);
return;
}
}
// pass the request along the filter chain
filterChain.doFilter(request, servletResponse);
}
private boolean isAllowedOrigin(String origin){
for (String allowedOrigin : allowedOrigins) {
if(origin.equals(allowedOrigin)) return true;
}
return false;
}
}
// 스프링 서버 전역적으로 CORS 설정
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080", "http://localhost:8081") // 허용할 출처
.allowedMethods("GET", "POST") // 허용할 HTTP method
.allowCredentials(true) // 쿠키 인증 요청 허용
.maxAge(3000) // 원하는 시간만큼 pre-flight 리퀘스트를 캐싱
}
}
// 특정 컨트롤러에만 CORS 적용하고 싶을때.
@Controller
@CrossOrigin(origins = "*", methods = RequestMethod.GET)
public class customController {
// 특정 메소드에만 CORS 적용 가능
@GetMapping("/url")
@CrossOrigin(origins = "*", methods = RequestMethod.GET)
@ResponseBody
public List<Object> findAll(){
return service.getAll();
}
}