CORS(Cross-Origin-Resource Sharing, 교차 출처 리소스 공유)는 서버에게 다른 오리진으로부터의 요청을 승인하게 하는 보안 매커니즘이다.
여기서 오리진이란, URL에서 프로토콜(스킴), 도메인(주소), 포트의 결합을 말한다.
https://example.com:3000
https://example.com:8080
-> 같은 프로토콜과 도메인을 사용하지만, 포트번호가 다르므로 서로 다른 오리진이다.
http://naver.com(:80 생략)
https://naver.com(:443 생략)
-> 같은 도메인을 사용하지만, 프로토콜과 포트번호가 다르므로 서로 다른 오리진이다.
https://google.com/search?query=cors
https://google.com/user
-> 같은 프로토콜, 도메인, 포트번호를 사용하므로 서로 같은 오리진이다.
보안상의 이유로, 서로 다른 출처(오리진)간의 공유는 기본적으로 금지되어있다.
악의적인 웹 사이트가 데이터를 탈취하는 것을 막기 위해서이다.
이를 SOP(Same-Origin Policy)라고 한다.
클라이언트 측에서 응답 HTTP를 읽고 응답을 받을지 기각할지 정한다.
응답을 받을 화이트리스트를 서버가 관리하고, 클라이언트는 응답의 화이트리스트에 자신이 없으면 응답을 받지 않는다.
웹의 초기에서는, 동일한 서버에서 웹 애플리케이션 서비스와 정적 파일들을 제공하는 것을 모두 하였기 때문인데, 서로 다른 오리진 간에 데이터를 공유할 이유가 없었다.
그러나 오늘날은 여러 서비스들과 API를 공유하기도 하고, 모바일 앱도 있고, SPA(Single Page Application)가 발달하면서,
서로 다른 출처의 클라이언트와 API서버간에 통신이 필요해져서 CORS와 관련된 HTTP 응답 헤더
를 통해서 특정 클라이언트가 다른 오리진으로부터 응답을 받을 수 있도록 SOP의 제한을 완화하는 것이다.
CORS는 W3C와 IETF에 의해 표준화되었으며, 이를 통해서 서로 다른 출처간의 자원 공유를 안전하게 수행하도록 한다.
일부 요청은 CORS Preflight가 필요없다.
이러한 요청들을 simple requests라 한다.
simple requests는 다음의 HTTP 메소드를 가져야 가진다:
GETHEADPOST그리고, 아래와 같이 자동으로 유저 에이전트에 의해 의해 붙는 헤더들(CORS-safelisted request header)만을 가진다:
Content-Type의 경우, 다음의 유형만을 가져야 한다:
application/x-www-form-urlencodedmultipart/form-datatext/plain아래는 simple requests의 CORS 요청-응답의 사이클의 예시이다:

Access-Control-Allow-Origin: *을 통해서 모든 오리진으로부터 허용함을 의미하는 헤더를 붙인다.Access-Control-Allow-Origin: *을 확인하고, 클라이언트는 자신이 받아도 되는 응답임을 확인한 뒤 응답을 받아서 처리한다.HTTP 요청 예시:
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
HTTP 응답 예시:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
[…XML Data…]
위의 예시에서는 Access-Control-Allow-Origin의 값을 *로 설정해서 모든 오리진에 대해 허용했지만, 오리진을 직접 명시해서 화이트리스트를 만들 수 있다.
// 모든 오리진에 대해 허용
Access-Control-Allow-Origin: *
// https://foo.example에 대해서만 허용
Access-Control-Allow-Origin: https://foo.example
🔔주의
credential requests, 즉 민감한 요청에 대한 응답을 할 때에는, 반드시 직접 허용할 오리진을 명시시켜야 한다.
*와일드카드는 사용할 수 없다.
이외에 preflighted requests, 즉 사전 요청이 필요한 HTTP 요청은, 요청 본문을 바로 보내지 않는다.
우선, OPTIONS 메서드로 다른 오리진에게 실제 요청을 보내도 안전할지에 대해서 미리 요청을 보낸다.

위 그림에서의 예시를 보면, X-PINGOTHER라는 비표준 HTTP요청 헤더가 있는 걸 볼 수 있는데, 일반적으로 이러한 헤더들은 웹 애플리케이션에서 응용하기에 좋다.
요청의 Content-Type이 application/json이기에, 사전 요청을 미리 보낸 모습이다.
실제 Main request에서는 Access-Control-Request-*가 붙지 않은 것을 볼 수 있다.
Preflighted request로는 OPTIONS가 사용되는데, 이는 리소스를 변경할 수 없는 안전한 메소드이기 때문이다.
그와 동시에, 두 개의 헤더가 최소한 더 붙는다:
Access-Control-Request-Method: 실제 보낼 요청의 HTTP 메서드를 담는다.Access-Control-Request-Headers: 실제 보낼 요청에서 HTTP 요청에 붙일 요청 헤더들의 정보를 알린다.그에 대한 응답으로는
Access-Control-Allow-Origin: 서버가 보낸 응답을 받을 수 있는 오리진Access-Control-Allow-Methods: 허용할 메서드. ,으로 구분되어 복수개가 올 수 있다.Access-Control-Allow-Headers: 허용할 요청 헤더. ,으로 구분되어 복수개가 올 수 있다.Access-Control-Max-Age: Preflight request의 응답에 대한 캐싱 기간.Preflight Request의 요청-응답 사이클이 정상적으로 동작하면, 실제 요청-응답 사이클이 돌아간다.
모든 브라우저가 preflighted request를 리다이렉션 시키는 것을 지원하지는 않는다.
일부 브라우저는 다음과 같은 에러 메시지를 던진다:
The request was redirected to https://example.com/foo, which is disallowed for cross-origin requests that require preflight. Request requires preflight, which is disallowed to follow cross-origin redirects.
이를 해결하기 위해서는, 서버 사이드에서 preflight의 리다이렉션을 잘 핸들링하도록 바꾸거나,
가능하다면 요청을 simple request로 바꾸는 방법이 있다.
쿠기를 보낼 수도 있고, Authorization 관련 정보를 보낼 수도 있다.
이러한 경우에는, 민감한 정보를 교환하기 위해
Access-Control-Allow-Credentials: true 헤더를 응답에 붙여야 한다.
비록 요청의 쿠키 헤더가 서버 측 오리진의 콘텐츠를 가리키더라도, Access-Control-Allow-Credentials가 true로 되어있지 않으면, 응답을 받을수는 없다.
CORS Preflight request는 민감한 정보를 담지 않는다.
대신, Main에서는 민감한 정보가 오갈 것이므로, Preflight Request에 대한 응답에서는 Access-Control-Allow-Credentials를 true로 설정해야 한다.
그래야 Main Request에서 정상적으로 정보를 교환한다.
민감한 요청(Credential Request)을 응답할 때는, 다음의 규칙을 지키자:
Access-Control-Allow-Origin에서 *을 붙이면 안된다.Access-Control-Allow-headers, Access-Controls-Allow-Mehtods에도 적용된다.Access-Control-Expose-Headers에 *을 사용할 수 없다.https://brownbears.tistory.com/337
글을 참고했다.
사실 Gin에서 공식적으로 지원해주는 CORS 설정 패키지가 있는데, 내 프로젝트에서는 동작하지 않았다.
package middlewares
import (
"os"
"github.com/gin-gonic/gin"
)
// CORSMiddleware - Append Cors-related Headers
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", os.Getenv("CLIENT_ORIGIN"))
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin, Accept, Host, User-Agent, Referer, Access-Control-Request-Headers, Access-Control-Request-Method, Sec-Fetch-Mode, Refresh-Token, Accept-Encoding, Accept-Language, Connection, Content-Length")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Methods", "GET, DELETE, POST, PUT, OPTIONS")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
다음의 미들웨어를 모든 라우터의 전역으로 최우선 미들웨어로 설정해주었다.
이 미들웨어에서는 허용에 필요한 모든 조건들을 충족시키고 있다.
만약, 모든 오리진에 허용하고 싶은데, 민감한 정보를 다루고 싶다면, 다음의 편법이 있다:
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
if origin != "" {
c.Header("Access-Control-Allow-Origin", origin)
}
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin, Accept, Host, User-Agent, Referer, Access-Control-Request-Headers, Access-Control-Request-Method, Sec-Fetch-Mode, Refresh-Token, Accept-Encoding, Accept-Language, Connection, Content-Length")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Methods", "GET, DELETE, POST, PUT, OPTIONS")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
요청 헤더에서 클라이언트의 오리진을 가져와서, 허용할 오리진에 넣는 방법이다.
Access-Control-Allow-Origin에는 하나의 오리진 또는 와일드카드만 가능한데, 여러 오리진으로부터 허용하고 싶다면, 다음과 같은 방법이 있다:
"허용할 오리진들"을 따로 저장해 두었다가(메모리에 저장할지, DB에 저장할지 등은 알아서 각자의 판단에..), 목록에서 요청의 오리진과 대조해서 클라이언트의 오리진이 발견되면, 허용할 오리진으로 요청의 오리진에 넣으면 되겠다.
코드는 생략한다.