개요
- cors에 대해서 자세히 알아본다.
- gin web framework 사용 시 cors 처리 예시를 작성해본다.
CORS 란(참고)
- 정의
- CORS(Cross-Origin Resource Sharing, CORS)는 추가적인 HTTP 헤더를 사용하여,
다른 출처의 선택한 자원에 접근할 수 있도록 브라우저에게 알려주는 체제이다.
- CORS(Cross-Origin Resource Sharing, CORS)는 추가적인 HTTP 헤더를 사용하여,
- 자세한 설명
- 보안 상의 이유로 브라우저는 스크립트에서 시작한 다른 서버로의 HTTP 요청을 제한한다.
- 예를들어 기본적으로 XMLHttpRequest 또는 FetchAPI는 동일한 서버의 리소스만 불러올 수 있다.
- 만약 다른 서버 리소스를 불러오고 싶다면 그 서버에서 올바른 CORS 헤더를 포함한 응답을 해야한다.
HTTP 요청이 다른 서버에 온 것을 어떻게 아는가?
- HTTP 헤더 중 Origin을 보고 판별한다.
- Origin은 fetch가 시작된 위치를 의미하는 헤더이다(참고)
- Origin은 프로토콜, 호스트, 포트 중 하나라도 다르면 다른 Origin으로 인식된다.
- 예를 들어, http://localhost:8080과 http://localhost:8888은 서로 포트가 다르므로 다른 Origin이다.
- Origin 문법(참고)
1 2
Origin: null Origin: <scheme> "://" <hostname> [ ":" <port> ]
- <scheme>
- 사용하는 프로토콜. 일반적으로 HTTP 프로토콜 혹은 보안 버전인 HTTPS를 사용합니다.
- <hostname>
- 서버(가상 호스팅)의 이름 또는 IP 입니다.
- <port> Optional
- 서버와 연결을 맺기 위한 TCP 포트 번호.
- 포트번호를 입력하지 않으면, 요청한 서비스의 기본 포트(HTTP의 경우 “80”)가 사용됩니다.
CORS가 필요한 상황
- API 서버와 프론트엔드 서버를 따로 둘 때 필요하다.
- 요즘은 확장성, 모바일 환경 대응 등의 여러 이유로 API 서버와 프론트엔드 서버를 따로 분리시키는 경우가 많다.
- API 서버와 프론트엔드 서버를 통합하여 라우팅해주는 부분이 없다면 CORS 처리를 해줘야한다.
용어 정리
- script의 XMLHttpRequest 또는 FetchAPI 요청을 ‘AJAX 요청‘이라고 명명한다.
- 앞으로의 예제에서 AJAX 요청을 하는 서버를 ‘프론트 서버‘로
프론트 서버의 요청을 받아 CORS 헤더와 함께 응답을 하는 서버를 ‘API 서버‘로 명명한다.
구제적 상황 1. 단순 요청(참고)
- API 서버가 요청을 받았을 때, 이미 흔하게 웹에서 사용하고 있는 간단한 요청으로 판명하면
Origin 헤더만 체크하여 API 서버에서 허용한 Origin인 경우에만 정상값을 리턴한다. - 간단한 요청으로 판명하는 근거는 다음과 같다
- 다음 중 하나의 메서드
- GET
- HEAD
- POST
- 유저 에이전트가 자동으로 설정한 헤더만 있는 경우
- Accept
- Accept-Language
- Content-Language
- Content-Type
(Content-Type은 아래 값인 경우에만 단순 요청으로 판단)- application/x-www-form-urlencoded
- multiplart/form-data
- text/plain
- 다음 중 하나의 메서드
단순 요청 예시 환경 구축 및 실행
- 예시 소스 코드(참고)
- 상황
- API 서버 호스트: localhost:8889
- 프론트 서버 호스트: localhost:8888
- 프론트 서버 test.html에서 script의 fetchAPI를 이용하여 API 서버로 요청을 보낸다.
- 요청정보
- method: POST
- endpoint: /test
- 요청정보
- API 서버 라우터
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.POST("test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success"}) }) router.Run(":8889") }
- 프론트 서버 test.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
{{ define "test.html" }} <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <meta content="yes" name="apple-mobile-web-app-capable" /> <meta content="minimum-scale=1.0, width=device-width, maximum-scale=1, user-scalable=no" name="viewport" /> </head> <body> <button onclick="testPost()">POST 요청</button> <script type="text/javascript"> const baseURL = "http://localhost:8889"; const testURL = `${baseURL}/test`; function testPost() { fetch(testURL, { method: "POST", }) .then((response) => response.json()) .then((responseJson) => console.log("Response", JSON.stringify(responseJson)) ) .catch((error) => console.log("Error", error)); } </script> </body> </html> {{ end }}
- 실행방법
- api 서버 실행
1 2 3
# 새 bash shell을 엶 cd api go run main.go
- 프론트 서버 실행
1 2 3
# 새 bash shell을 엶 cd front go run main.go
- 브라우저에서 script의 fetchAPI를 이용하여 API 서버로 POST 요청
- 브라우저를 켬
- http://localhost:8888로 접속
- f12를 눌러 관리자 모드를 활성화하고 Console 클릭
- ‘POST 요청’ 버튼을 눌러 실험 시작
- api 서버 실행
단순 요청 예시 실험 결과
단순 요청 예시 개선
- API 서버 응답에 Access-Control-Allow-Origin 헤더에 CORS를 허용하는 Origin을 추가하여 리턴하면 된다.
- Access-Control-Allow-Origin 헤더 예시
- http://localhost:8888만 허용
- Access-Control-Allow-Origin: http://localhost:8888
- 모든 도메인 허용
- Access-Control-Allow-Origin: *
- http://localhost:8888만 허용
- 요청 객체에서 Origin을 판별하여 허용 Origin인 경우
Access-Control-Allow-Origin 헤더를 추가하는 방식으로 구현할 수 있다(구현 예시) - 여기서는 직접 구현 대신 Official CORS gin’s middleware를 사용한다.
- API 서버 라우터
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
package main import ( "net/http" "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() // <<< -- 변경 내역 시작 -- <<< router.Use(cors.New( cors.Config{ AllowOrigins: []string{"http://localhost:8888"}, AllowMethods: []string{"POST"}, MaxAge: 12 * time.Hour, })) // >>> -- 변경 내역 끝 -- >>> router.POST("test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success"}) }) router.Run(":8889") }
단순 요청 예시 개선 결과
구제적 상황 2. 사전 요청(Preflight request)(참고)
- 단순 요청이 아닌 그 외의 모든 요청은 사전 요청을 해야한다(참고).
- Client(보통 브라우저)에서 API 서버가 단순 요청으로 판단하지 않는 메서드, 헤더, Origin을 포함하여
AJAX 요청을 하는 경우, Client는 본 요청 전에 사전 요청을 해야한다. - 만약 Client가 브라우저라면 사전 요청을 자동으로 진행한다.
- 사전 요청은 본 요청의 메서드, 헤더, Origin을 CORS 헤더에 담아 OPTIONS 메서드로 요청한다.
- 사전 요청의 헤더는 다음과 같다.
- Access-Control-Request-Method
- 본 요청의 메서드를 포함한다.
- ex. Access-Control-Request-Method: DELETE
- Access-Control-Request-Headers
- 본 요청의 헤더를 포함한다.
- ex. Access-Control-Request-Headers: origin, x-custom-header
- Origin
- 본 요청 및 사전 요청의 Origin
- ex. Origin: https://localhost:8888
- Access-Control-Request-Method
사전 요청 예시 환경 구축 및 실행
- custom-header라는 임의의 헤더를 포함하여 API 서버에 AJAX 요청을 할 것이다.
- ‘단순 요청 성공 예시’에서 아래 내용만 수정하여 이전과 같은 방법으로 실행한다.
- 프론트 서버 test.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
{{ define "test.html" }} <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <meta content="yes" name="apple-mobile-web-app-capable" /> <meta content="minimum-scale=1.0, width=device-width, maximum-scale=1, user-scalable=no" name="viewport" /> </head> <body> <button onclick="testPost()">POST 요청</button> <script type="text/javascript"> const baseURL = "http://localhost:8889"; const testURL = `${baseURL}/test`; function testPost() { fetch(testURL, { method: "POST", // <<< -- 변경 내역 시작 -- <<< headers: { "custom-header": "1", }, // >>> -- 변경 내역 끝 -- >>> }) .then((response) => response.json()) .then((responseJson) => console.log("Response", JSON.stringify(responseJson)) ) .catch((error) => console.log("Error", error)); } </script> </body> </html> {{ end }}
사전 요청 예시 결과
- 실패
사전 요청 예시 개선
- API 서버에서 CORS 허용하는 메서드, 헤더, Origin의 CORS 헤더를 응답에 담아 보내도록 설정해야한다.
- 서버의 사전 요청 대응 헤더는 다음과 같다.
- Access-Control-Allow-Origin
- 서버에서 CORS 허용하는 Origin
- 본 요청 응답에 담긴다.
- ex. Access-Control-Allow-Origin: http://localhost:8080
- Access-Control-Allow-Methods
- 서버에서 CORS 허용하는 메서드
- 사전 요청 응답에 담긴다.
- ex. Access-Control-Allow-Methods: POST, GET, OPTIONS
- Access-Control-Allow-Headers
- 서버에서 CORS 허용하는 헤더
- 사전 요청 응답에 담긴다.
- ex. Access-Control-Allow-Headers: custom-header, cookie
- Access-Control-Allow-Origin
- 역시 Official CORS gin’s middleware를 사용한다.
- API 서버 라우터
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
package main import ( "net/http" "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.Use(cors.New( cors.Config{ AllowOrigins: []string{"http://localhost:8888"}, AllowMethods: []string{"POST"}, // <<< -- 변경 내역 시작 -- <<< AllowHeaders: []string{"Origin", "custom-header"}, // >>> -- 변경 내역 끝 -- >>> MaxAge: 12 * time.Hour, })) router.POST("test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success"}) }) router.Run(":8889") }
사전 요청 예시 개선 결과
- 성공
구제적 상황 3. 인증정보가 포함된 요청(참고)
- 기본적으로 AJAX 요청에는 인증정보가 포함되지 않는다.
- 인증정보는 쿠키, authorization 헤더들 또는 TLS 클라이언트 인증서이다(참고).
- 인증정보를 AJAX 요청에 담아서 보내는 경우 Credentials 인자를 조정해야한다.
- XMLHttpRequest의 경우 withCredentials 인자를 true로(참고) 할당해야하고,
fetch의 경우 credentials 인자를 ‘include’로(참고) 할당해야한다. - Client(보통 브라우저)가 인증정보를 담아 API 서버로 AJAX 요청을 하면,
API 서버에서는 Client 요청에 문제가 없는 경우 Access-Control-Allow-Credentials: true 헤더를
포함하여 응답한다. - Client는 응답에 Access-Control-Allow-Credentials: true가 포함되지 않으면 거부한다.
- !주의 - API 서버에서 CORS Credential를 허용하는 경우, 모든 Origin에게 CORS를 허용할 수 없다.
‘Access-Control-Allow-Origin: *‘와 같이 와일드카드를 사용할 수 없고 직접 Origin을 입력해야한다.
인증정보가 포함된 요청 예시 환경 구축 및 실행
- ‘name: auth, value: 1’인 쿠키를 프론트 서버에서 할당하고 API 서버로 AJAX 요청을 보낸다.
- ‘사전 요청 성공 예시’에서 아래 내용만 수정하여, 이전과 같은 방법으로 실행한다.
- 프론트 서버 test.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
{{ define "test.html" }} <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <meta content="yes" name="apple-mobile-web-app-capable" /> <meta content="minimum-scale=1.0, width=device-width, maximum-scale=1, user-scalable=no" name="viewport" /> </head> <body> <button onclick="testPost()">POST 요청</button> <script type="text/javascript"> const baseURL = "http://localhost:8889"; const testURL = `${baseURL}/test`; // <<< -- 변경 내역 시작 -- <<< function setCookie() { // 참고 : https://cofs.tistory.com/363 var date = new Date(); date.setTime(date.getTime() + 60 * 60 * 24 * 1000); document.cookie = `auth=1; expires=' + ${date.toUTCString()}; path=/`; } // >>> -- 변경 내역 끝 -- >>> function testPost() { // <<< -- 변경 내역 시작 -- <<< setCookie() // >>> -- 변경 내역 끝 -- >>> fetch(testURL, { method: "POST", headers: { "custom-header": "1", }, credentials: "include", }) .then((response) => response.json()) .then((responseJson) => console.log("Response", JSON.stringify(responseJson)) ) .catch((error) => console.log("Error", error)); } </script> </body> </html> {{ end }}
인증정보가 포함된 요청 예시 결과
- 실패
인증정보가 포함된 요청 예시 개선
- 역시 Official CORS gin’s middleware를 사용한다.
- API 서버 라우터
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
package main import ( "net/http" "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.Use(cors.New( cors.Config{ AllowOrigins: []string{"http://localhost:8888"}, AllowMethods: []string{"POST"}, AllowHeaders: []string{"Origin", "custom-header"}, // <<< -- 변경 내역 시작 -- <<< AllowCredentials: true, // >>> -- 변경 내역 끝 -- >>> MaxAge: 12 * time.Hour, })) router.POST("test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success"}) }) router.Run(":8889") }