출처가 무엇을 뜻하는지 알아보자
출처가 무엇인지 알기 위해서 먼저 URL의 구조를 살펴보자
프로토콜의 HTTP는 80번, HTTPS는 443번 포트를 사용하는데, 80번과 443번 포트는 생략하다
출처란 URL 구조에서 살펴본 Protocol,Host,Port를 합친 것을 말한다. 브라우저 개발자 도구의 콘솔 창에 location.origin
을 실행하면 출처를 확인할 수 있다.
현재 웹페이지의 주소가 https://beomy.github.io/tech/
일 때 같은 출처인지 다른 출처인지 아래 테이블과 같은 결과를 얻을 수 있습니다.
POSTMAN으로 API를 테스트하거나, 다른 서버에서 API를 호출할때는 멀쩡히 잘 동작하다가 브라우저에서 API를 호출할때만
CORS policy
오류가 발생했던 경험이 있을 수도 있다. 그 이유는 브라우저가 동일 출처 정책(Same-Origin Policy, SOP)를 지켜서 다른 출처의 리소스 접근을 금지하기 때문이다. 하지만 실제로 웹페이즈는 상당히 자주 다른 출처의 리소스를 사용해야 한다. 예를 들어beomy.github.io
라는 도메인 주소를 사용하는 웹페이지에서beomy-api.github.io
라는 API 서버로 데이터를 요청해서 화면을 그린다면 이 웹페이지는 동일 출처 정책을 위반한 것이 된다.
CORS의 동장방식은 단순 요청 방법과 예비 요청을 먼저 보내는 방법 2가지 방법이 있다.
단순한 요청은 서버에 API를 요청하고, 서버는 Access-Control-Allow-Origin
헤더를 포함한 응답을 브라우저에 보낸다.
브라우저는 Access-Control-Allow-Origin
헤더를 확인해서 헤더를 확인해서 CORS 동작을 수행할지 판단한다.
서버로 전달하는 요청(request)는 아래의 3가지 조건을 만족해야 서버로 전달하는 요청이 단순 요청으로 동작한다.
요청 메서드
: 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 중 하나를 사용해야 한다.첫번째 조건은 어렵지 않은 조건이지만 2번,3번조건은 좀 까다롭다. 2번 조건은 사용자 인증에 사용되는 Authorization
헤더도 포함되지 않아 까다로운 조건이며, 3번 조건은 많은 REST API
들이 Content-Type
으로 application/json
을 사용하기 때문에 지켜지기 어려운 조건이다.
Preflight request는 서버에 예비 요청을 보내서 안전한지 판단한 후 본 요청을 보내는 방법이다. 아래 그림은 아래 그림은 Preflight 요청 동작을 나타내는 그림이다.
GET,POST,PUT,DELETE 등의 메서드로 API를 요청했는데, 크롬 개발자 도구의 네트워크 OPTIONS 메서드로 요청이 보내지는 것은 CORS를 지키기 위해서이다.
Preflight 요청은 실제 리소스를 요청하기 전에 OPTIONS라는 메서드를 통해 실제 요청을 전송할지 판단한다.
OPTION 메서드로 서버에 예비 요청을 먼저 보내고, 서버는 이 예비 요청에 대한 응답으로 Access-Control-Allow-Origin
헤더를 포함한 응답을 브라우저에 보낸다.
브라우저는 단순 요청과 동일하게 Access-Control-Allow-Origin
헤더를 확인해서 CORS 동작을 수행할지 판단한다.
Access-Control-Allow-Origin
헤더를 포함한 응답을 브라우저에게 보내는 방식으로 CORS 에러를 해결할 수 있다. Access-Control-Allow-Origin
헤더를 포함한 응답을 하는 등 몇가지 응답 헤더를 포함해 달라고 요청해야 한다.Node.js의 Express는 cors라는 서드 파트 미들웨어를 지원합니다. 이 라이브러리에서 CORS 응답 헤더를 추가해 주기 때문에, 개발자가 별도의 CORS 응답 헤더를 추가해 주지 않아도 됩니다. 다른 프레임워크에서도 CORS를 해결해 주는 라이브러리가 존재합니다.
라이브러리를 사용하면 간단하게 CORS를 해결할 수 있지만, CORS를 해결하기 위한 응답 헤더가 무엇이 있는지 하나씩 살펴보자.
아래와 같이 응답 헤더가 작성되었다면 https://beomy.github.io
페이지에서 브라우저는 서버 응답으로 온 리소스를 접근할 수 있다
Access-Control-Allow-Origin: https://beomy.github.io
아래와 같이 *(와일드 카드)가 작성되었다면, 브라우저는 출처에 상관없이 모든 리소스에 접근할 수 있다
Access-Control-Allow-Origin: *
아래 코드를 브라우저에서 실행하여 Access-Control-Allow-Origin
헤더를 처리하지 않은 서버에 API를 호출하게 되면,
fetch('http://localhost:3001/cors', {
method: 'PUT',
}).then(function(response) {
}).catch(function(error) {
})
아래와 같은 에러가 발생한다.
Access-Control-Allow-Methods: <method>[, <method>]*
브라우저에서 보내는 요청 헤더에 포함된 Access-Control-Request-Method
헤더에 대한 응답결과이다.
리소스 접근을 허용하는 HTTP 메서드를 지정해 주는 헤더이다.
사용방법
Access-Control-Allow-Methods
헤더에 GET, PUT, POST, DELETE 등의 HTTP 메서드를 ,로 구분하여 넘겨주면 된다.Access-Control-Allow-Methods: GET, PUT
아래 코드를 브라우저에서 실행하여 Access-Control-Allow-Methods
헤더를 처리하지 않은 서버에 API를 호출하게 되면,
fetch('http://localhost:3001/cors', {
method: 'PUT',
}).then(function(response) {
}).catch(function(error) {
})
router.options('/cors', (req, res, next) => {
res.set('Access-Control-Allow-Origin', '*')
res.set('Access-Control-Allow-Methods', req.get('Access-Control-Request-Method'))
res.send()
})
router.put('/cors', (req, res, next) => {
res.set('Access-Control-Allow-Origin', '*')
res.send()
})
Access-Control-Allow-Origin
는 *
로 모든 출처를 허용한 상태이고, 브라우저의 요청 헤더에 포함된 Access-Control-Request-Method
헤더 값을 그대로 Access-Control-Allow-Methods
헤더에 추가해 주었다.Access-Control-Request-Method
헤더는 HTTP 요청 헤더에서 설명하도록 하겠다.Access-Control-Expose-Headers: <header-name>[, <header-name>]*
서버에서 응답 헤더에 Access-Control-Expose-Headers
를 추가해 줘야 브라우저의 자바 스크립에서 헤더에 접근할 수 있다.
사용방법
,
로 구분하여 여러 개의 헤더를 넣을 수 있다.Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
예시
router.options('/cors', (req, res, next) => {
res.set('Access-Control-Allow-Origin', '*')
res.set('Access-Control-Allow-Methods', req.get('Access-Control-Request-Method'))
res.send()
})
router.put('/cors', (req, res, next) => {
res.set('Access-Control-Allow-Origin', '*')
res.set('Access-Control-Expose-Headers', 'X-Custom-Beomy')
res.set('X-Custom-Beomy', 'Bemoy')
res.send()
})
Access-Control-Expose-Headers
헤더에 X-Custom-Beomy
를 추가해 주고, X-Custom-Beomy
헤더에 값을 담아 응답을 하면,fetch('http://localhost:3001/cors', {
method: 'PUT',
}).then(function(response) {
console.log(response.headers.get('X-Custom-Beomy')) // Beomy
}).catch(function(error) {
})
브라우저에서는 위 코드를 실행해서, X-Custom-Beomy 헤더 값을 가져올 수 있게 된다.
서버에서 Access-Control-Expose-Headers: X-Custom-Beomy
로 자바스크립트에서 접근할 헤더를 명시해 주지 않으면, 자바스크립트에서 X-Custom-Beomy
헤더 값은 undefined
가 된다.
Access-Control-Allow-Headers: <header-name>[, <header-name>]*
브라우저에서 보내는 요청 헤더에 포함된 Access-Control-Request-Headers
헤더에 대한 응답 결과이다.
사용 방법
OPTIONS
요청 헤더의 Access-Control-Request-Headers
헤더에 커스텀 헤더 이름이 추가됩니다. Access-Control-Request-Headers
에 작성된 값을 보고 Access-Control-Allow-Headers
응답 헤더에 커스텀 헤더 이름을 명시해 주어야 한다.Access-Control-Allow-Headers: X-Custom-Request
예시
Access-Control-Allow-Headers
처리되지 않은 API를 호출하게 되면,fetch('http://localhost:3001/cors', {
method: 'PUT',
headers: {
'X-Custom-Request': 'Beomy',
}
}).then(function(response) {
}).catch(function(error) {
})
router.options('/cors', (req, res, next) => {
res.set('Access-Control-Allow-Origin', '*')
res.set('Access-Control-Allow-Methods', req.get('Access-Control-Request-Method'))
res.set('Access-Control-Allow-Headers', req.get('Access-Control-Request-Headers'))
res.send()
})
router.put('/cors', (req, res, next) => {
res.set('Access-Control-Allow-Origin', '*')
console.log(req.get('X-Custom-Request')) // Beomy
res.send()
})
X-Custom-Request
헤더에 Beomy
값을 서버에 전달하였고, 서버에서는 Access-Control-Allow-Headers
헤더에 Access-Control-Request-Headers
헤더 값을 저장하여 서버에서 X-Custom-Request
값을 사용할 수 있게 한 코드이다.위에 헤더 뿐만 아니라 Access-Control-Max-Age: <delta-seconds>
, Access-Control-Allow-Credentials: true
헤더들이 존재한다.
CORS를 위해서, 브라우저에서 서버로 요청하는 헤더를 살펴보자. 요청 헤더들은 별도로 명시해 주지 않아도 브라우저에서 OPTIONS 요청에 추가한다.
Origin: <origin>
Origin
헤더는 요청하는 대상의 출처를 나타냅니다. API를 호출하는 페이지의 출처 값이 저장됩니다.
Access-Control-Request-Method: <method>
Access-Control-Request-Method
헤더는 실제 요청이 어떤 HTTP 메서드를 사용하는지 서버에 알려주기 위해 사용됩니다.
Access-Control-Request-Headers: <field-name>[, <field-name>]
Access-Control-Request-Headers
헤더는 브라우저에서 보내는 커스텀 헤더 이름을 서버에 알려주기 위해 사용됩니다.
@CrossOrigin
어노테이션 사용하기@CrossOrigin
어노테이션을 제공한다.@CrossOrigin
은 모든 출처, 모든 헤더, @RequestMapping
주석에 지정된 HTTP 메서드에 최대 30분을 허용한다. 어노테이션에 속성 값을 넣어 기본 값을 대체할 수 있다.@CrossOrigin(origin="*", allowedHeaders = "*")
@Controller
public class MainController {
@GetMapping(path = "/")
public String main(Model model) {
return "main";
}
CorsFilter
사용하기@Component
public class SimpleCorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT, PATCH");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, remember-me");
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
}