CORS

dragonappear·2022년 1월 26일
0

HTTP

목록 보기
3/3


출처


목차

  • CORS란?
  • 동일 출처 정책(Same-Origin Policy)이란?
  • CORS 동작원리
  • CORS 에러 해결 방법

CORS란?

  • 위 오류는 CORS 정책을 위반할 때 발생하게 된다.
  • CORS는 Cross-Origin Resource Sharing의 약자로 교차 출처 리소스 공유를 뜻하는데, 브라우저에서 다른 출처의 리소스를 공유할때를 말한다.

URL

출처가 무엇을 뜻하는지 알아보자

출처가 무엇인지 알기 위해서 먼저 URL의 구조를 살펴보자

프로토콜의 HTTP는 80번, HTTPS는 443번 포트를 사용하는데, 80번과 443번 포트는 생략하다

출처

출처란 URL 구조에서 살펴본 Protocol,Host,Port를 합친 것을 말한다. 브라우저 개발자 도구의 콘솔 창에 location.origin을 실행하면 출처를 확인할 수 있다.

같은 출처 VS 다른 출처

현재 웹페이지의 주소가 https://beomy.github.io/tech/일 때 같은 출처인지 다른 출처인지 아래 테이블과 같은 결과를 얻을 수 있습니다.


동일 출처 정책(Same-Origin Policy)이란?

POSTMAN으로 API를 테스트하거나, 다른 서버에서 API를 호출할때는 멀쩡히 잘 동작하다가 브라우저에서 API를 호출할때만 CORS policy 오류가 발생했던 경험이 있을 수도 있다. 그 이유는 브라우저가 동일 출처 정책(Same-Origin Policy, SOP)를 지켜서 다른 출처의 리소스 접근을 금지하기 때문이다. 하지만 실제로 웹페이즈는 상당히 자주 다른 출처의 리소스를 사용해야 한다. 예를 들어 beomy.github.io라는 도메인 주소를 사용하는 웹페이지에서 beomy-api.github.io라는 API 서버로 데이터를 요청해서 화면을 그린다면 이 웹페이지는 동일 출처 정책을 위반한 것이 된다.

동일 출처 정책의 장점

  • 동일 출처 정책을 지키면 외부 리소스를 가져오지 못해 불편하지만, 동일 출처 정책은
    XSSXSRF 등의 보안 취약점을 노릭 공격을 방어할수있다.
  • 하지만 현실적으로 외부 리소스를 참고하는 것은 필요하기 때문에 외부 리소스를 가져올 수 있는 방법이 존재해야 한다. 외부 리소스를 사용하기 위한 SOP의 예외조항이 CORS이다.

CORS 동작원리

CORS의 동장방식은 단순 요청 방법과 예비 요청을 먼저 보내는 방법 2가지 방법이 있다.

Simple request

  • 단순 요청 방법은 서버에게 바로 요청을 보내는 방법이다. 아래 그림은 자바스크립트에서 API를 요청할 때 브라우저와 서버의 동작을 나타내는 그림이다.

  • 단순한 요청은 서버에 API를 요청하고, 서버는 Access-Control-Allow-Origin
    헤더를 포함한 응답을 브라우저에 보낸다.

  • 브라우저는 Access-Control-Allow-Origin 헤더를 확인해서 헤더를 확인해서 CORS 동작을 수행할지 판단한다.

Simple request 조건

서버로 전달하는 요청(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 request는 서버에 예비 요청을 보내서 안전한지 판단한 후 본 요청을 보내는 방법이다. 아래 그림은 아래 그림은 Preflight 요청 동작을 나타내는 그림이다.

  • GET,POST,PUT,DELETE 등의 메서드로 API를 요청했는데, 크롬 개발자 도구의 네트워크 OPTIONS 메서드로 요청이 보내지는 것은 CORS를 지키기 위해서이다.

  • Preflight 요청은 실제 리소스를 요청하기 전에 OPTIONS라는 메서드를 통해 실제 요청을 전송할지 판단한다.

  • OPTION 메서드로 서버에 예비 요청을 먼저 보내고, 서버는 이 예비 요청에 대한 응답으로 Access-Control-Allow-Origin 헤더를 포함한 응답을 브라우저에 보낸다.

  • 브라우저는 단순 요청과 동일하게 Access-Control-Allow-Origin 헤더를 확인해서 CORS 동작을 수행할지 판단한다.


CORS 에러 해결 방법

  • 앞에서 이야기한 CORS 동작 원리르 보면, 서버에서 Access-Control-Allow-Origin 헤더를 포함한 응답을 브라우저에게 보내는 방식으로 CORS 에러를 해결할 수 있다.
  • 프론트엔드 개발자가 CORS 에러를 확인다면, 서버에게 Access-Control-Allow-Origin 헤더를 포함한 응답을 하는 등 몇가지 응답 헤더를 포함해 달라고 요청해야 한다.

Node.js의 Express는 cors라는 서드 파트 미들웨어를 지원합니다. 이 라이브러리에서 CORS 응답 헤더를 추가해 주기 때문에, 개발자가 별도의 CORS 응답 헤더를 추가해 주지 않아도 됩니다. 다른 프레임워크에서도 CORS를 해결해 주는 라이브러리가 존재합니다.

HTTP 응답헤더

라이브러리를 사용하면 간단하게 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) {
})

아래와 같은 에러가 발생한다.

  • 서버에서 아래와 같이 응답 헤더를 추가해 주어야 한다.
  • 서버 코드는 Node.js의 Express로 작성되었다.
  • 와일드카드를 사용하여 모든 출처에서 리소스를 접근할 수 있도록 설정

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 헤더들이 존재한다.

HTTP 요청 헤더

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 헤더는 브라우저에서 보내는 커스텀 헤더 이름을 서버에 알려주기 위해 사용됩니다.


CORS 해결

클라이언트에서 해결하는 법

  • 웹 브라우저 실행 옵션이나 플러그인을 통한 동일 출처 정책 회피하기
    • 동일 출처 정책은 브라우저에서 임의로 하는 것이기 때문에 브라우저에서 동일 출처 정책을 사용하지 않으며 된다.
  • jsonp 방식으로 json 데이터 가져오기
    • 자바스크립트 파일이나 css 파일은 동일 출처 정책에 영향을 받지 않고 가져올 수 있다.
    • 이를 이용해서 자바스크립트 파일을 가져와서 이를 json 형식으로 파싱해서 데이터를 사용할 수 있다.

서버에서 해결하는 법

@CrossOrigin 어노테이션 사용하기

  • 메서드 레벌 및 글로벌 레벨에서 Spring MVC 애플리케이션에서 Spring CORS를 지원하는 방법이다.
  • Spring MVC는 @CrossOrigin 어노테이션을 제공한다.
  • 이 어노테이션은 어노테이션이 달린 메서드 또는 타입을 교차 출처를 허용하는 것으로 표시한다.
  • 기본적으로 @CrossOrigin은 모든 출처, 모든 헤더, @RequestMapping 주석에 지정된 HTTP 메서드에 최대 30분을 허용한다. 어노테이션에 속성 값을 넣어 기본 값을 대체할 수 있다.

속성값

  • origin
    • 허용된 출처, 이 값은 pre-flight 응답과 access-control-origin 헤더에 배치된다
  • allowdHeaders
    • 실제 요청 중에 사용할 수 있는 요청 헤더 목록이다. pre-flight의 응답 헤더인 access-control-allow-header에 값이 사용된다.
  • allowCredential
    • 브라우저가 요청과 관련된 쿠키를 포함해야 되는지 여부를 결정한다.
    • 이 값이 true이면, pre-flight 응답에는 값이 true로 설정된 access-control-allow-credentials 헤더가 포함된다.
@CrossOrigin(origin="*", allowedHeaders = "*")
@Controller
public class MainController {
	@GetMapping(path = "/")
	public String main(Model model) {
		return "main";
	}

CorsFilter 사용하기

  • 서블릿 필터 인터페이스를 이용하여 개발되었다.
  • 웹 서버의 모든 리소스의 요청을 가로채서 Cross domain request인지 체크하여 실제 요청 페이지에 전달하기전에 적절한 CORS 정책과 헤더들을 적용한다.

속성

  • Access-Control-Allow-Origin
    • 도메인 간 요청을 할 수 있는 권한이 부여된 도메인을 지정한다.
  • Access-Control-Allow-Credentials
    • 도메인 간 요청에 credential 권한이 있는지 없는지 지정한다.
  • Access-Control-Expose-Headers
    • 노출하기에 안전한 헤더를 나타낸다.
  • Access-Control-Max-Age
    • pre-flighted 요청이 얼마만큼의 시간동안 캐시되는지
  • Access-Control-Allow-Methods
    • 리소스에 접근할 때 메소드가 허용되는지
  • Access-Control-Allow-Headers
    • 어떤 헤더 필드 네임이 실제 요청에서 사용할 수 있는지 가리킨다.
@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() {
    }
}

0개의 댓글