CORS 작동 방식 3가지 시나리오
이전 포스팅에서 SOP 그리고 CORS의 동작 흐름에 대해서 포스팅하였다.
이번 포스팅에서는 CORS의 기본 동작 흐름말고 CORS가 동작하는 세 가지 시나리오와 그 해결법에 대해서 알아보고자 한다.
예비 요청(PreFlight Request)
우리가 흔히 접하게 되는 CORS 동작 방식이다. 웹 브라우저는 요청을 보낼 때 사실 한 번에 바로 보내지 않고, 먼저 예비 요청을 보내고 난 뒤 서버와 잘 통신되는지 확인을 한 후에 본 요청을 보낸다.
이 때, 예비 요청의 역할은 본 요청을 보내기 전에 브라우저 스스로 안전한 요청인지 미리 확인하는 것이다.
브라우저가 예비요청을 보내는 것을 PreFlight라고 부르고, 예비요청의 HTTP 메서드는 OPTIONS 메서드이다.
OPTIONS METHOD
HTTP 메서드의 OPTIONS 메서드는 대상 리소스에 대한 통신 옵션을 설명하는 데 사용되는 메서드이다. 보안상의 이유로 데이터를 다른 도메인으로 보낼 때 preFlight 요청이 수행되는데 이 때 수행하는 HTTP 메서드이다.
OPITONS 메서드를 통해서 클라이언트는 리소스에 대한 작업을 수행하거나 리소스를 요청하지 않고도 특정 리소스 및 서버 기능에 대한 매개 변수와 요구 사항 또한 얻을 수 있다.
예비 요청의 수행 과정
1. 자바스크립트의 fetch() 메서드를 통해 리소스를 받아오려고 한다.
2. 브라우저는 서버로 HTTP OPTIONS 메소드로 예비 요청(Preflight)을 먼저 보낸다.
- a. Origin 헤더에 자신의 출처를 넣는다.
- b. Access-Control-Request-Method 헤더에 실제 요청에 사용할 메소드를 설정한다.
- c. Access-Control-Request-Headers 헤더에 실제 요청에 사용할 헤더들을 설정한다.
3. 서버는 이 예비 요청에 대한 응답으로 어떤 것을 허용하고 어떤것을 금지하고 있는지에 대한 헤더 정보를 담아서 브라우저로 보내준다.
- a. Access-Control-Allow-Origin 헤더에 허용되는 Origin들의 목록을 설정한다.
- b. Access-Control-Allow-Methods 헤더에 허용되는 메소드들의 목록을 설정한다.
- c. Access-Control-Allow-Headers 헤더에 허용되는 헤더들의 목록을 설정한다.
- d. Access-Control-Max-Age 헤더에 해당 예비 요청이 브라우저에 캐시 될 수 있는 시간을 초 단위로 설정한다.
4. 이후 브라우저는 보낸 요청과 서버가 응답해준 정책을 비교하여, 해당 요청이 안전한지 확인하고 본 요청을 보내게 된다.
5. 서버가 본 요청에 대한 응답을 하면 최종적으로 이 응답 데이터를 자바스크립트로 넘겨준다.
예비 요청의 문제점과 캐싱
요청을 보내기 전에 OPTIONS 메서드를 통해서 적절한 요청인지 확인하는 방식은 좋지만, 추가 요청이 발생하는 것이기 때문에 실제 요청에 걸리는 시간이 늘어나게 되어 애플리케이션 성능에 큰 영향을 미친다는 단점이 있다.
API 호출 수가 많게 되면 그 만큼 요청이 배로 증가하게 되니 그 단점 또한 더욱 부가되는데 위에서 서버에 응답 헤더에 제시했던 브라우저 캐시를 사용해서 PreFlight 요청을 캐싱시켜서 위와 같은 단점을 최적화 하는 것이 가능하다.
다만, 웹 브라우저에 따라서 캐시 적용 최대 가능시간이 다르기 때문에 웹 브라우저에서 허용되는 시간을 보고 적절한 캐싱 시간을 정해주는 것이 중요하다.
참고로 크로미엄 기반 브라우저 v76 이전은 10분 v76 이후는 7200초(2시간), 파이어폭스 브라우저는 24시간 이라고 한다.
예비 요청의 캐싱 동작 방식은 다른 캐싱 매커니즘과 유사하게 동작한다고 한다.
- 브라우저는 예비(Preflight) 요청을 할 때마다, 먼저 Preflight 캐시를 확인하여 해당 요청에 대한 응답이 있는지 확인한다.
- 만일 응답이 캐싱 되어 있지 않다면, 서버에 예비 요청을 보내 인증 절차를 밟는다.
- 만일 서버로 부터 Access-Control-Max-Age 응답 헤더를 받는다면 그 기간 동안 브라우저 캐시에 결과를 저장한다.
- 다시 요청을 보내고 만일 응답이 캐싱 되어 있다면, 예비 요청을 서버로 보내지 않고 대신 캐시된 응답을 사용한다.
단순 요청(SImple Request)
단순 요청은 말그대로 예비 요청(Prefilght)을 생략하고 바로 서버에 본 요청을 보낸 후, 서버가 이에 대한 응답 헤더에 Access-Control-Allow-Origin 헤더를 보내주면 브라우저가 CORS정책 위반 여부를 검사하는 방식이다.
위의 PreFlight와 다른 점은 단지 예비 요청이 빠진 것 뿐이다. 따라서, 속도면에서 엄청나게 향상 됐을 것이라고 생각이 드는데, 다만 단순 요청을 수행하기 위한 많은 조건들이 존재한다.
- 요청의 메소드는 GET, HEAD, POST 중 하나여야 한다.
- Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width 헤더일 경우 에만 적용된다.
- Content-Type 헤더가 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나여야한다. 아닐 경우 예비 요청으로 동작된다.
하지만 요근래에는 text/xml 이나 application/json 으로 많은 통신이 이루어지기 때문에 Simple Request 요청은 거의 수행되지 않는다. 이러한 이유로 위에 주로 마주할 수 있는 요청 시나리오를 PreFlight Request 라고 한 것이다.
인증된 요청(Credentialed Request)
인증된 요청은 클라이언트에서 서버에게 자격 인증 정보(Credential)를 실어 요청할 때 사용되는 요청이다.
위에서 얘기한 자격 인증 정보는 세션 ID가 저장되어 있는 쿠키 혹은 Authorization 헤더에 설정하는 토큰 값 등에 해당한다.
기존의 예비 요청, 단순 요청에 비해 좀 더 빡빡하게 동작하는 방식이다.
1. 클라이언트에서 인증 정보를 보내도록 설정하기
기본적으로 브라우저가 제공하는 요청 API 들은 별도의 옵션 없이 브라우저의 쿠키와 같은 인증과 관련된 데이터를 함부로 요청 데이터에 담지 않도록 되어 있다.
이때 요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 바로 credentials 옵션이다. 해당 옵션에는 3가지 값을 지정할 수 있다.
옵션 값 | 설명 |
same-origin(기본값) | 같은 출처 간 요청에만 인증 정보를 담을 수 있다. |
include | 모든 요청에 인증 정보를 담을 수 있다. |
omit | 모든 요청에 인증 정보를 담지 않는다. |
이러한 별도의 설정이 없으면 쿠키 등의 인증 정보는 절대로 자동으로 서버에게 전송되지 않는다고 한다.
서버에 인증된 요청을 보내는 방법이 라이브러리에 따라서 다양하고 어떤 메서드를 사용하느냐에 따라 그 credentials 옵션을 전달하는 문법이 다르다고 한다. 이를 살펴보자.
// fetch 메서드
fetch("https://example.com:1234/users/login", {
method: "POST",
credentials: "include", // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
body: JSON.stringify({
userId: 1,
}),
})
// axios 라이브러리
axios.post('https://example.com:1234/users/login', {
profile: { username: username, password: password }
}, {
withCredentials: true // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
})
// jQuery 라이브러리
$.ajax({
url: "https://example.com:1234/users/login",
type: "POST",
contentType: "application/json; charset=utf-8",
dataType: "json",
xhrFields: {
withCredentials: true // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
},
success: function (retval, textStatus) {
console.log( JSON.stringify(retval));
}
});
2. 서버에서 인증된 요청에 대한 응답 헤더 설정하기
클라이언트에서 보내는 요청이 기존이랑 다른 것처럼 서버에서 인증된 요청에 대한 응답 헤더를 설정하는 방법도 다르다.
- 응답 헤더의 Access-Control-Allow-Credentials 항목을 true로 설정해야 한다.
- 응답 헤더의 Access-Control-Allow-Origin의 값에 와일드카드 문자("*")는 사용할 수 없다.
- 응답 헤더의 Access-Control-Allow-Methods 의 값에 와일드카드 문자("*")는 사용할 수 없다.
- 응답 헤더의 Access-Control-Allow-Headers 의 값에 와일드카드 문자("*")는 사용할 수 없다.
제일 중요한 부분은 Access-Control-Allow-Credentials 항목을 꼭 true로 해주어야 한다는 것이다.
기존의 방식보다 정말 엄격한 인증을 거치는 것이기 때문에 와일드 카드 문자는 사용이 불가능하다.
위 과정또한 PreFlight 과정과 동일하다. 다만 Cookie 혹은 Authorization 헤더에 다른 값이 추가로 요청헤더에 들어가고 응답헤더에 Access-Control-Allow-Credentials 가 생긴 것만이 차이점이다.
CORS 해결 방법
1. Chrome 확장 프로그램 이용
Chrome에서는 CORS 문제를 해결하기 위한 확장 프로그램을 제공해준다고 한다.
아래 링크에서 크롬 확장 프로그램 설치가 가능하다.
다만 localhost 환경에서 API 테스트 시 CORS 문제를 해결하는 방식이라 근본적인 해결은 아니다.
2. 프록시 사이트 이용
프록시(Proxy)란 기본적으로 대리자라는 의미를 지니고 있다. 이를 어떤 분야에 이용하느냐에 따라서 다르게 적용된다고 한다.
따라서, 프록시 사이트란 다음과 같이 해석할 수 있다. HTTP 요청을 처리하는 과정을 대신해서 수행해주는 사이트
CORS에서는 다음과 같이 사용이 된다.
프론트에서 직접 서버 리소스를 요청 했는데 서버에서 따로 설정을 안해줘서 CORS 에러가 뜬다면 모든 출처를 허용한 서버 대리점을 통해 요청을 처리하는 것.
다만 정의를 보니 상당히 악용될 가능성이 많은 것처럼 보인다. 누군가는 일부러 CORS 에러를 띄우려고 의도한 것일수도 있는데 프록시 서비스를 이용하여 우회 할 수도 있기 떄문이다. 따라서, 현재 무료 프록시 서버 대여 서비스들은 모두 악용 사례 때문에 api 요청 횟수 제한을 두어 실전에서는 사용하기 무리라고 한다.
테스트용이나 맛보기용으로 사용하고 실전에서는 직접 프록시 서버를 구축해서 사용해야 한다고 한다.
heroku 프록시 서버
http://cors-anywhere.herokuapp.com/corsdemo
위의 사이트로 가서 버튼을 누르고 데모 서버를 활성화 시키면 사용가능하다고 한다.
다만, 시간 제한이 있기 때문에 일시적인 해결방편이다.
const url = 'https://google.com' // 이 부분을 이용하는 서버 URL로 변경
fetch(`https://cors-anywhere.herokuapp.com/${url}`)
.then((response) => response.text())
.then((data) => console.log(data));
요청 시 다음과 같이 요청을 보내면 된다.
cors proxy app 프록시 서버
raravel님이 만든 프록시 서비스라고 한다. axios 라이브러리 설치가 필요하다.
<script src='https://cdnjs.cloudflare.com/ajax/libs/axios/1.1.3/axios.min.js'></script>
<script>
axios({
url: 'https://cors-proxy.org/api/',
method: 'get',
headers: {
'cors-proxy-url' : 'https://google.com/' // 이 부분을 이용하는 서버 URL로 변경
},
}).then((res) => {
console.log(res.data);
})
</script>
cors.sh 프록시 서버
가장 실전에서 사용하기 좋은 프록시 서버이다. 다만, 실전에서 이용하려면 유료로 결제를 해야한다. free로도 어느정도 사용이 가능하다고 한다.
const url = 'https://google.com' // 이 부분을 이용하는 서버 URL로 변경
fetch(`https://proxy.cors.sh/${url}`)
.then((response) => response.text())
.then((data) => console.log(data));
서버에서 Access-Control-Allow-Origin 헤더 세팅하기
하지만 위의 프록시 서버를 이용하는 것보다도 가장 좋은 것은 서버에서 직접 CORS에 대한 응답 헤더를 설정해주는 것이다.
# 헤더에 작성된 출처만 브라우저가 리소스를 접근할 수 있도록 허용함.
# * 이면 모든 곳에 공개되어 있음을 의미한다.
Access-Control-Allow-Origin : https://naver.com
# 리소스 접근을 허용하는 HTTP 메서드를 지정해 주는 헤더
Access-Control-Request-Methods : GET, POST, PUT, DELETE
# 요청을 허용하는 해더.
Access-Control-Allow-Headers : Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization
# 클라이언트에서 preflight 의 요청 결과를 저장할 기간을 지정
# 60초 동안 preflight 요청을 캐시하는 설정으로, 첫 요청 이후 60초 동안은 OPTIONS 메소드를 사용하는 예비 요청을 보내지 않는다.
Access-Control-Max-Age : 60
# 클라이언트 요청이 쿠키를 통해서 자격 증명을 해야 하는 경우에 true.
# 자바스크립트 요청에서 credentials가 include일 때 요청에 대한 응답을 할 수 있는지를 나타낸다.
Access-Control-Allow-Credentials : true
# 기본적으로 브라우저에게 노출이 되지 않지만, 브라우저 측에서 접근할 수 있게 허용해주는 헤더를 지정
Access-Control-Expose-Headers : Content-Length
Node.js 세팅
var http = require('http');
const PORT = process.env.PORT || 3000;
var httpServer = http.createServer(function (request, response) {
// Setting up Headers
response.setHeader('Access-Control-Allow-origin', '*'); // 모든 출처(orogin)을 허용
response.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); // 모든 HTTP 메서드 허용
response.setHeader('Access-Control-Allow-Credentials', 'true'); // 클라이언트와 서버 간에 쿠키 주고받기 허용
// ...
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.end('ok');
});
httpServer.listen(PORT, () => {
console.log('Server is running at port 3000...');
});
위의 예시를 보면 Access-Control-Allow-origin 을 와일드카드로 열어둔 것을 볼 수 있다. 위 처럼 지정하는 것은 이상한 출처에서 오는 요청까지 전부 허용하기 때문에 보안에서 허술해진다는 문제가 있다. 따라서, origin에서는 항상 사용하고자 하는 출처를 명시해주자.
Express.js 세팅
> npm i cors
const express = require('express')
const cors = require("cors"); // cors 설정을 편안하게 하는 패키지
const app = express();
// ...
app.use(cors({
origin: "https://naver.com", // 접근 권한을 부여하는 도메인
credentials: true, // 응답 헤더에 Access-Control-Allow-Credentials 추가
optionsSuccessStatus: 200, // 응답 상태 200으로 설정
}));
Servlet 세팅
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;
}
}
아래 메서드를 보면 Origin을 검증하는 로직이 추가 되어있다. 개발자가 직접 이러한 부분에 대해서 하드코딩해서 origin을 설정해두도록 하자. 하지만 AWS 같은 외부 서버 사용시에는 문제가 발생할 수 있기 때문에 application.properties 혹은 application.yml 에 값을 지정하고 @Value를 통해 값을 받아오는 형식으로 민감한 데이터는 숨기도록 하자.
Spring 세팅
// 스프링 서버 전역적으로 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();
}
}
Apache 세팅
httpd.conf의 <IfModule mod_headers.c> 태그 내부 혹은 VirtualHost 부분에 넣어준다.
<IfModule mod_headers.c>
Header set Access-Control-Allow-Origin "*"
</IfModule>
Tomcat 세팅
톰캣 폴더 경로의 conf/web.xml 에서 설정해준다.
<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
<init-param>
<param-name>cors.allowed.origins</param-name>
<param-value>*</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.methods</param-name>
<param-value>GET,POST,HEAD,OPTIONS,PUT,DELETE</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.headers</param-name>
<param-value>Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>
</init-param>
<init-param>
<param-name>cors.exposed.headers</param-name>
<param-value>Access-Control-Allow-Origin,Access-Control-Allow-Credentials</param-value>
</init-param>
<init-param>
<!-- 쿠키 통신을 안하는데 이걸 true로 하면 4XX 서버 에러가 뜬다 -->
<param-name>cors.support.credentials</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>cors.preflight.maxage</param-name>
<param-value>10</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Nginx
nginx.conf 파일 안에 location / 부분에 add_header 값으로 헤더 설정을 추가한다.
location / {
root html;
add_header 'Access-Control-Allow-Origin' '*';
index index.html index.htm;
}
참고 출처
https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F#cors_%EC%9E%91%EB%8F%99_%EB%B0%A9%EC%8B%9D_3%EA%B0%80%EC%A7%80_%EC%8B%9C%EB%82%98%EB%A6%AC%EC%98%A4
https://evan-moon.github.io/2020/05/21/about-cors/
https://reqbin.com/Article/HttpOptions
'Computer Science > Network' 카테고리의 다른 글
웹 서버와 WAS의 차이, 그리고 아파치와 NGINX 알아보기 (0) | 2023.05.12 |
---|---|
CORS의 개념과 작동방식 (0) | 2023.05.09 |