Posts gonic-gin CORS 처리
Post
Cancel

gonic-gin CORS 처리

개요

  • cors에 대해서 자세히 알아본다.
  • gin web framework 사용 시 cors 처리 예시를 작성해본다.

CORS 란(참고)

  • 정의
    • 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에서 CORS 헤더 설정 부분이 없기 때문에 자신 외 다른 Origin에서 요청이 거부된다.

단순 요청 예시 개선

  • 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: *
  • 요청 객체에서 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")
        }
      

단순 요청 예시 개선 결과

  • 성공
    • 응답 헤더에서 Access-Control-Allow-Origin: http://localhost:8888를 받아오는 것을 확인할 수 있다.

구제적 상황 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

사전 요청 예시 환경 구축 및 실행

  • 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를 허용하지 않은 custom-header 헤더가 요청에 포함되어 실패하였다.
    • 아래 사진에서 사전 요청(OPTIONS 메서드 요청)이 실패한 것을 확인할 수 있다.

사전 요청 예시 개선

  • 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
  • 역시 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")
        }
      

사전 요청 예시 개선 결과

  • 성공
    • 사전 요청 응답
      • Access-Control-Allow-Methods, Access-Control-Allow-Headers 헤더가 추가되었다.
    • 본 요청 응답
      • Access-Control-Allow-Origin 헤더가 추가되었다.

구제적 상황 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 }}
      
      

인증정보가 포함된 요청 예시 결과

  • 실패
    • AJAX 요청에는 인증정보가 담겼지만,
      API 서버에서는 Crendentials 관련 세팅이 되어있지 않아 에러가 났다.
    • Access-Control-Allow-Credentials가 포함되도록 세팅해주면 에러가 해결된다.

인증정보가 포함된 요청 예시 개선

  • 역시 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")
        }
      

인증정보가 포함된 요청 예시 결과

  • 성공
    • Access-Control-Allow-Credentials 헤더가 추가되어있다.

참고

This post is licensed under CC BY 4.0 by the author.