继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

聊聊CORS那些事儿

HUX布斯
关注TA
已关注
手记 339
粉丝 84
获赞 378

嗨,新年快乐!希望你在假期里有机会好好休息,甚至堆了几个雪人也说不定。

前几天,我和一个朋友(我们叫她Eowyn)聊了聊,她最近刚踏入软件工程领域:

伊欧温: 嘿,兄弟!我正在建立一个网页项目,这个项目包含前端和后端部分。每当我点击UI上的按钮(触发到服务器的DELETE请求时),浏览器就会弹出这些错误:

我用 curl 命令试了同样的端点,也能成功。我在 Postman 里也试了一次,同样成功了。这真奇怪!这到底是怎么回事?

我: 嘿嘿,恭喜恭喜,这真是你职业生涯中的一个大日子——你发现了CORS的那一天!哈哈

值得称赞的是,她不想让我帮忙解决这个问题,而是想要理解这个问题。所以我花了接下来半小时来解释CORS这个概念。而且这也不是我第一次做这样的解释,我意识到这是一次写一篇关于这个主题的文章的好机会,下次就可以分享链接而不是再做一次解释。

免责声明:这篇帖子标题为理解CORS,今天我们的目标正是要达到这一目的。如果你对CORS不太了解,这是一篇适合你的文章。我们不会深入讲解,也不会涉及所有细节和特殊情况——如果你想了解更多,可以参考这篇来自Mozilla的深度文章。在这里,你将了解在遇到类似错误时背后发生了什么。

实际案例

如果你经常看我的博客,你可能会注意到我喜欢通过现实生活中的例子来解释技术方面的内容。我相信这是理解起来最简单快捷的方法。今天我们也会这样做,就像以前一样。

让我来介绍给你认识,杰洛特,一名猎魔人,他是这一行里数一数二的高手。因为他的技能高超以及野外危险生物众多,杰洛特的服务非常抢手。人们联系他如此频繁,以至于他只好找了名叫鲍勃的秘书帮忙。因此,一旦有人打电话给杰洛特的办公室,接电话的都是鲍勃。一般情况下,流程是这样的:

如果有人打来杰拉尔特办公室的电话,鲍勃会接电话,先记录来电者的相关信息(如来电者是谁,来电原因,来电者的位置),并让他们稍等一下。

  • 鲍勃给杰洛特打电话,分享了关于来电者的消息,并问他是否愿意和他们聊聊

正如我们可以看到的,格尔阿尔特回答说,如果接下来两小时内有人从因根海姆打来电话询问打猎事宜,鲍勃应该立刻转接。

  • 鲍勃回拨来电者,接通了杰洛特。

  • 如果在接下来的2小时内,有来自不是英格海姆的地方的另一来电,Bob就会拒绝将他们与Geralt接通。

然而,如果有人(比如像猎魔人的好友雅尔彭一样)知道杰洛特的直接联系方式,这样他们仍然可以不管在哪里或出于什么原因给他打电话。

我认为这应该很清楚。但它怎么会跟CORS有关呢?

从真实生活到软件工程

如果我们试图将我们用到的现实生活例子对应到软件工程的概念中,它看起来会像这样:

  • 给办公室打电话的人 — 前端程序
  • Bob — 浏览器端
  • Geralt — 后端程序

步骤如下:

  • 当前端应用尝试向后端 API 发送请求时,浏览器会向后端 API 发送所谓的预检请求,询问哪些请求是被允许的,例如谁可以调用 API 以及可以发送什么类型的请求
  • API 会返回这些选项,并且(可选地)告诉浏览器这些设置能缓存多久
  • 如果前端应用和它的请求都在允许列表中,浏览器会允许通过
  • 否则,请求会被拒绝,并显示与文章开头提到的相同的错误

然而,绕过浏览器直接发送请求(例如使用curlPostman或其他任何HTTP客户端工具)可以轻松绕过此机制——这就是Yarpen直接拨打Geralt的个人电话而不是办公室电话所做的事情。

我们是否可以假设CORS是一个相当糟糕的安全机制,因为我们很容易绕过它?答案是“这要看情况”。如果我们希望只允许授权服务调用我们的API,CORS不是一个单一解决方案,因为它不适用于服务器间的通信。CORS主要用于防止跨站请求伪造(CSRF)攻击。我们来讨论一下它吧。

CSRF (跨站请求伪造)

想象今天是发薪日,所以你刚刚登录了你的网上银行账户查看余额,发现钱已经到账——真好!于是你开心地打开了社交媒体动态开始浏览。突然,有一个“惊喜”:有人发布了一个链接。描述说你最爱的那种泡菜正在大促销——这真是你的好日子,不是吗?你点击了链接,却发现那里没有你想要的泡菜,只是一个空白页面。“这真不公平!怎么会有人开这种玩笑?”于是你心想,然后关闭了那个页面。

假设你在访问“泡菜诈骗”页面时(即使页面是空白的),你在监控你的网络流量,你就会注意到,实际上包含了一小段JavaScript脚本,这段脚本向你的银行API发起请求,试图将钱转账到一个你不知道的账户上。因为你在不知情的情况下已经登录了在线银行账户,所以银行并没有意识到你是在不知情的情况下进行了转账。但是,如果银行启用了CORS安全策略,浏览器会验证“泡菜诈骗”这个域名是否有权限调用银行的API,由于它没有权限,请求会被拒绝。虽然你没有得到泡菜,但你的钱还是安全的。

我让ChatGPT给我多举一些CSRF攻击的例子,以下是它给出的示例:

示例一:换电子邮件地址

  1. 情形:Alice 登录了她的 emailservice.com 邮箱账号。
  2. 攻击:她接着访问了一个恶意网站 malicioussite.com,该网站包含一个隐藏表单,该表单被 JavaScript 自动提交。该网站发起一个 POST 请求,以更改她的邮箱设置(例如她的恢复邮箱地址)。
  3. 结果:如果 emailservice.com 没有充分的 CSRF 保护,它可能误认为这是 Alice 主动提交的请求,从而导致她的恢复邮箱被更改,而她自己却不知情。

示例 2:社交媒体上的动态

  1. 情景:鲍勃登录了一个社交媒体平台。
  2. 攻击:他点击了一个链接,进入了恶意网站。该网站包含了一个脚本,该脚本要求社交媒体平台向鲍勃的所有联系人发送消息。
  3. 结果:如果社交媒体平台没有验证请求,鲍勃的账户可能会被用来发送垃圾信息或恶意信息。

例子 3:修改密码

  1. 情境:Dana 登录了一个论坛账号。
  2. 攻击:她收到一封带有有趣文章链接的电子邮件。点击链接后,她会被带到一个隐藏表单的网站,该网站发送一个更改她密码的请求。
  3. 结果:如果没有 CSRF 防护,Dana 的密码可能在她不知情的情况下被改掉,这可能导致她无法登录她的账号。

正如你所见,如果服务器配置了CORS(跨源资源共享),这些问题本可以避免。但是,我们该如何去做呢?让我们来看看代码吧!

一一些代码

如我们已经确定的那样,这里有三种类型的演员。

  • 浏览器
  • 前端
  • 后端

在这个例子中,我们将使用Brave(一个基于Chromium的浏览器)来为了简化用户界面。现在我们开始看代码吧。

后端开发

我们将构建一个微型书籍API,包含三个端点:

  • 获取所有书
  • 添加新书
  • 删除所有书

这是一个测试应用,所以我们会把所有数据存放在内存里。以下是我们的后端全部Go代码:

    package main  

    import (  
     "encoding/json"  
     "errors"  
     "fmt"  
     "github.com/go-chi/chi/v5"  
     "net/http"  
    )  

    var books = []string{"The Lord of the Rings", "The Hobbit", "The Silmarillion"}  

    type Book struct {  
     Title string `json:"title"`  
    }  

    func main() {  
     err := runServer()  
     if err != nil {  
      if errors.Is(err, http.ErrServerClosed) {  
       fmt.Println("服务器已关闭运行")  
      } else {  
       fmt.Println("服务器失败:错误信息为 %v", err)  
      }  
     }  
    }  

    func runServer() error {  
     httpRouter := chi.NewRouter()  

     httpRouter.Route("/api/v1", func(r chi.Router) {  
      r.Get("/books", 获取所有书籍)  
      r.Post("/books", 添加书籍)  
      r.Delete("/books", 删除所有书籍)  
     })  

     server := &http.Server{Addr: "localhost:8888", Handler: httpRouter}  
     return server.ListenAndServe()  
    }  

    // 获取所有书籍
    func 获取所有书籍(w http.ResponseWriter, req *http.Request) {  
     respBody, err := json.Marshal(books)  
     if err != nil {  
      w.WriteHeader(http.StatusInternalServerError)  
      return  
     }  

     w.Header().Set("Content-Type", "application/json")  
     w.WriteHeader(http.StatusOK)  
     w.Write(respBody)  
    }  

    // 添加书籍
    func 添加书籍(w http.ResponseWriter, req *http.Request) {  
     var book Book  
     err := json.NewDecoder(req.Body).Decode(&book)  
     if err != nil {  
      w.WriteHeader(http.StatusBadRequest)  
      return  
     }  

     books = append(books, book.Title)  

     w.WriteHeader(http.StatusCreated)  
    }  

    // 删除所有书籍
    func 删除所有书籍(w http.ResponseWriter, req *http.Request) {  
     books = []string{}  

     w.WriteHeader(http.StatusNoContent)  
    }

你可以在n0rdy的GitHub仓库里找到它。

如你所见,我使用了一个外部依赖 github.com/go-chi/chi/v5 来构建此 API。完全可以用纯 Go 实现相同的功能,但我这样做是为了让代码更易读。除此之外,代码非常简单:它会读取、写入或删除书籍切片中的数据,并返回成功的响应。

让我们运行它:服务器将运行在 <http://localhost:8888>

现在是时候定义前端了:

前端技术

在这里我们需要一个简单的HTML页面,包含JS脚本来向后端API发送请求。这里还需要一个小型的Go服务器来提供页面。如下是HTML代码:

    <!DOCTYPE html>  
    <html lang="zh-CN">  
    <head>  
        <meta charset="UTF-8">  
        <meta name="viewport" content="width=device-width, initial-scale=1.0">  
        <title>书籍</title>  
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"  
              integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">  
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"  
                integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"  
                crossorigin="anonymous"></script>  
    </head>  
    <body>  
    <div class="container p-3">  
        <button type="button" class="btn btn-primary" id="getBooks">查看书籍</button>  
        <button type="button" class="btn btn-danger" id="deleteAllBooks">删除所有书目</button>  
        <br>  
        <br>  

        <form>  
            <div class="mb-3">  
                <label for="inputBookTitle" classAs="form-label">书名</label>  
                <input type="text" class="form-control" id="inputBookTitle">  
            </div>  
            <button type="submit" class="btn btn-primary">添加</button>  
        </form>  
    </div>  

    <script>  
      function getBooks () {  
        fetch('http://localhost:8888/api/v1/books')  
          .then(response => response.json())  
          .then(data => {  
            const booksList = document.querySelector('.books-list')  
            if (booksList) {  
              booksList.remove()  
            }  

            const ul = document.createElement('ul')  
            ul.classList.add('books-list')  
            data.forEach(book => {  
              const li = document.createElement('li')  
              li.innerText = book  
              ul.appendChild(li)  
            })  
            document.body.appendChild(ul)  
          })  
      }  

      function deleteAllBooks () {  
        fetch('http://localhost:8888/api/v1/books', {  
          method: 'DELETE'  
        })  
          .then(response => {  
            if (response.status === 204) {  
              getBooks()  
            } else {  
              const div = document.createElement('div')  
              div.innerText = '出错啦'  
              document.body.appendChild(div)  
            }  
          })  
      }  

      const getBooksButton = document.getElementById('getBooks')  
      const deleteAllBooksButton = document.getElementById('deleteAllBooks')  
      const input = document.querySelector('input')  
      const form = document.querySelector('form')  

      getBooksButton.addEventListener('click', () => getBooks())  
      deleteAllBooksButton.addEventListener('click', () => deleteAllBooks())  

      form.addEventListener('submit', (event) => {  
        event.preventDefault()  

        const title = input.value  

        fetch('http://localhost:8888/api/v1/books', {  
          method: 'POST',  
          headers: {  
            'Content-Type': 'application/json'  
          },  
          body: JSON.stringify({ title })  
        })  
          .then(response => {  
            if (response.status === 201) {  
              input.value = ''  
              getBooks()  
            } else {  
              const div = document.createElement('div')  
              div.innerText = '出错啦'  
              document.body.appendChild(div)  
            }  
          })  
      })  
    </script>  
    </body>  
    </html>

一个这样的 Go 服务器:

package main

import (
    "errors"
    "fmt"
    "github.com/go-chi/chi/v5"
    "net/http"
)

func main() {
    err := runServer()
    if err != nil {
        if errors.Is(err, http.ErrServerClosed) {
            fmt.Println("服务器已关闭")
        } else {
            fmt.Println("服务器启动失败", err)
        }
    }
}

func runServer() error {
    httpRouter := chi.NewRouter()

    httpRouter.Get("/", serveIndex)

    server := &http.Server{Addr: "localhost:3333", Handler: httpRouter}
    return server.ListenAndServe()
}

func serveIndex(w http.ResponseWriter, req *http.Request) {
    http.ServeFile(w, req, "20240103-cors/01-no-cors/client/index.html")
}

运行 Go 代码后,它将会把 HTML 页面供 http://localhost:3333

通过我的画作,你可能已经注意到我很有设计天赋,比如说,UI也是我的另一件杰作。

我们来试试我们现在有什么。

测试时

在浏览器中打开http://localhost:3333/,然后按Option+Command+I(在MacOS上),或者通过视图 -> 开发者 -> 开发者工具打开它。理想情况下,你应该能看到‘网络’标签和‘控制台’,如下所示:

我们试试添加一本《哈利波特》,然后点击“添加”。就这样!

这个错误看起来有些眼熟,对吧?虽然它和这篇帖子开头的错误不太一样,但它非常像。让我们试着弄清楚它到底是什么意思。

你可能已经注意到,我们的后端代码中完全没有提到CORS。确实如此,我们暂时还没有添加任何CORS相关的配置。但这对浏览器来说没有影响:它仍然尝试发起一个预检请求。

如果我们点击它,会显示更多详情,可以看到浏览器尝试对与添加书籍端点相同的路径发起了一个 OPTIONS 请求,并收到了一个 405 Method Not Allowed 响应,这也就说得通了,因为我们还没有在后端实现 OPTIONS 请求的处理。

如果我们暂时回到刚才的实际例子,事情是这样的。

  • 有人给杰拉特的办公室打电话,鲍勃接起了电话,收集了关于这次来电的信息(是谁打来的,为什么打来,以及他们是从哪里打来的),让他们稍等一下,并告诉杰拉特确认一下
  • 但是“休斯顿,我们有问题了”:杰拉特的电话关机了,这样鲍勃就无法得知杰拉特今天的安排了

鲍勃会想办法让打来电话的人联系杰洛特吗?还是说他会同意客户所需的服务,而不需要与猎魔人交谈?当然不是,鲍勃现在必须拒绝客户。同样的事情也发生在我们这边:由于浏览器不知道后端 API 的 CORS 配置,它会拒绝发送任何请求——安全第一嘛!

让我们把它修好吧!

调整时间

前端应用还是原来的,但后端我们需要做一些调整。

  • 增加一个新功能来启用CORS:
func enableCors(w http.ResponseWriter) {  
 // 指定哪些域名可以访问此API  
 w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3333")  

 // 指定哪些HTTP方法可以访问此API  
 w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")  

 // 指定哪些请求头可以访问此API  
 w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type")  

 // 指定浏览器缓存预检请求结果过期时间(秒)  
 w.Header().Set("Access-Control-Max-Age", strconv.Itoa(60*60*2))  
}

正如你看到的,我们已经设置了4个CORS配置。

  • 允许调用我们 API 的域名 — <http://localhost:3333>
  • 我们 API 支持的 HTTP 方法 — GET, POST, DELETE
  • 可以传递给我们的 API 的请求头 — Accept, Content-Type
  • 浏览器可以记住和缓存这些设置的时间为 2 小时(以秒为单位)

一个简短的声明:与CORS相关的头部还有更多,但这些已经足够理解。我将在本文末尾分享相关链接。

  • 引入一个 OPTIONS 端点,以及一个处理它的函数:
...
httpRouter.Route("/api/v1", func(r chi.Router) {  
    // 设置路由处理书籍的选项请求
    r.Options("/books", 处理跨域请求选项)  
    // 处理获取所有书籍的GET请求
    r.Get("/books", getAllBooks)  
    // 处理添加新书籍的POST请求
    r.Post("/books", addBook)  
    // 处理删除所有书籍的DELETE请求
    r.Delete("/books", deleteAllBooks)  
})  
...  
// 处理跨域请求选项函数
func 处理跨域请求选项(w http.ResponseWriter, req *http.Request) {  
    // 启用跨域资源共享(w)
    启用跨域资源共享(w)  
    // 设置HTTP响应状态码为成功(w)
    w.WriteHeader(http.StatusOK)  
}
  • 在现有函数中加入 enableCors 调用,例如:
// 获取所有书籍的函数
func getAllBooks(w http.ResponseWriter, req *http.Request) {  
    // 将书籍列表转换为JSON格式
    respBody, err := json.Marshal(books)  
    // 如果转换失败,返回500状态码
    if err != nil {  
        w.WriteHeader(http.StatusInternalServerError)  
        return  
    }  
    // 启用跨域资源共享
    enableCors(w)  
    // 设置响应头的Content-Type为application/json
    w.Header().Set("Content-Type", "application/json")  
    // 返回200状态码
    w.WriteHeader(http.StatusOK)  
    // 发送响应体
    w.Write(respBody)  
}

最终代码如下:

    package main  

    import (  
     "encoding/json"  
     "errors"  
     "fmt"  
     "github.com/go-chi/chi/v5"  
     "net/http"  
     "strconv"  
    )  

    var books = []string{"The Lord of the Rings", "The Hobbit", "The Silmarillion"}  

    type Book struct {  
     Title string `json:"title"`  
    }  

    func main() {  
     err := runServer()  
     if err != nil {  
      if errors.Is(err, http.ErrServerClosed) {  
       fmt.Println("服务器已关闭")  
      } else {  
       fmt.Println("服务器运行失败", err)  
      }  
     }  
    }  

    func runServer() error {  
     httpRouter := chi.NewRouter()  

     httpRouter.Route("/api/v1", func(r chi.Router) {  
      r.Options("/books", corsOptions)  
      r.Get("/books", getAllBooks)  
      r.Post("/books", addBook)  
      r.Delete("/books", deleteAllBooks)  
     })  

     server := &http.Server{Addr: "localhost:8888", Handler: httpRouter}  
     return server.ListenAndServe()  
    }  

    func corsOptions(w http.ResponseWriter, req *http.Request) {  
     enableCors(w)  
     w.WriteHeader(http.StatusOK)  
    }  

    func getAllBooks(w http.ResponseWriter, req *http.Request) {  
     respBody, err := json.Marshal(books)  
     if err != nil {  
      w.WriteHeader(http.StatusInternalServerError)  
      return  
     }  

     enableCors(w)  
     w.Header().Set("Content-Type", "application/json")  
     w.WriteHeader(http.StatusOK)  
     w.Write(respBody)  
    }  

    func addBook(w http.ResponseWriter, req *http.Request) {  
     var book Book  
     err := json.NewDecoder(req.Body).Decode(&book)  
     if err != nil {  
      w.WriteHeader(http.StatusBadRequest)  
      return  
     }  

     books = append(books, book.Title)  

     enableCors(w)  
     w.WriteHeader(http.StatusCreated)  
    }  

    func deleteAllBooks(w http.ResponseWriter, req *http.Request) {  
     books = []string{}  

     enableCors(w)  
     w.WriteHeader(http.StatusNoContent)  
    }  

    func enableCors(w http.ResponseWriter) {  
     // 指定哪些网站可以访问此 API  
     w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3333")  

     // 指定可以访问此 API 的方法(默认允许 GET 方法)  
     w.Header().Set("Access-Control-Allow-Methods", "POST, DELETE")  

     // 指定可以访问此 API 的头部信息  
     w.Header().Set("Access-Control-Allow-Headers", "Content-Type")  

     // 指定预检请求缓存时长(秒)  
     w.Header().Set("Access-Control-Max-Age", strconv.Itoa(60*60*2))  
    }

让我们运行这段代码来看看是否能运行正常。如果遇到任何问题,可以尝试重启前端和后端程序,甚至通过隐身模式访问http://localhost:3333页面,因为可能存在由浏览器缓存引起的问题。

如果你多次点击所有按钮,尝试添加一些书籍,并对它们进行获取或删除操作,你会发现它按预期运行。更重要的是,当你查看 Network 选项卡时,会发现总共有一次预检请求存在。

这并不让我们惊讶,因为我们才为此设置了Access-Control-Max-Age头。

我相信你已经对CORS很熟悉了。但让我们再进一步,尝试捣鼓一下CORS配置,看看这会如何再次打乱流程。

故意打断时间轴

我们将逐个修改并回滚每个 CORS (跨域资源共享) 配置。请记得,应用更改后需要重启前端和后端应用。那我们开始吧!加油加油加油!

Access-Control-Allow-Origin

我们的前端运行在 http://localhost:3333,而在 Access-Control-Allow-Origin 标头中的值正是这个。让我们把它改成其他的。

    func enableCors(w http.ResponseWriter) {  
     // 指定了哪些域名可以访问此 API  
     w.Header().Set("Access-Control-Allow-Origin", "http://example.com")  

     // 指定了可以访问此 API 的方法  
     w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")  

     // 指定了允许访问此 API 的头部  
     w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type")  

     // 指定了浏览器可以缓存此预检请求结果的时长(两小时)  
     w.Header().Set("Access-Control-Max-Age", strconv.Itoa(60*60*2))  
    }

让我们试试重启这些应用,看看会怎样。

嗯,结果正如预期:只有http://example.com可以调用API。更有趣的是,如果我们使用localhost但换了一个端口,我们仍然会收到错误信息:

同样的,对于 httphttps,CORS 相当严格:

不过,你也可以将 Access-Control-Allow-Origin 头部设为 *,来允许任何人(包括未知来源)访问你的 API

    func enableCors(w http.ResponseWriter) {  
     // 指定哪些网站可以访问此API  
     w.Header().Set("Access-Control-Allow-Origin", "*")  

     // 指定哪些方法可以访问此API  
     w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")  

     // 指定哪些头部信息可以访问此API  
     w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type")  

     // 指定浏览器可以缓存预检请求结果的有效时间(秒)  
     w.Header().Set("Access-Control-Max-Age", strconv.Itoa(60*60*2))  
    }

即使我们尝试在不同的端口上运行前端应用,它仍然可以通过CORS步骤成功。仅在你清楚自己在做什么的情况下使用此方法。

好的,把 Access-Control-Allow-Origin 恢复原来的值,我们继续下一个。

允许的HTTP请求方法

在开始处理这个头部之前,让我先和你分享一个重要的陷阱:默认情况下,无论设置如何,GET和POST方法都是被允许的。这意味着 w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")w.Header().Set("Access-Control-Allow-Methods", "DELETE") 有同样的效果。

那就是为什么不需要删除它们,然后疑惑应用程序为何还能运行。然而,我们试着去掉 DELETE

    func enableCors(w http.ResponseWriter) {  
     // 指定哪些域名可以访问此 API  
     w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3333")  

     // 指定哪些请求方式可以访问此 API  
     w.Header().Set("Access-Control-Allow-Methods", "GET, POST")  

     // 指定哪些头信息可以访问此 API  
     w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type")  

     // 指定浏览器缓存预检请求结果的有效时间(以秒为单位)  
     w.Header().Set("Access-Control-Max-Age", strconv.Itoa(60*60*2))  
    }

一旦我们重启这两个应用后,我们会发现获取书和添加新书都没有问题,但删除它们时会遇到一个错误。

如果你的流程正常运行没有问题,那么可能是浏览器缓存(详见此处:the 2nd hardest thing)在捣乱你的体验。要解决这个问题,你可以尝试以下任意一种方法:

  • 取消选中“网络”标签下的“禁用缓存”选项
  • 打开一个新的无痕窗口

正如我们所见,错误信息清楚地表明了DELETE 方法未被预检请求响应中的 Access-Control-Allow-Methods 允许——我们就知道了,不是吗?

让我们先恢复这些值,然后处理下一个标题。

Access-Control-Allow-Headers (允许访问控制头)

我们代码中并没有明确使用 Accept 头,因此还是保留它吧。但在我们发送 POST 请求时,会用到 Content-Type 头。你知道该怎么处理它了 =)

    func enableCors(w http.ResponseWriter) {  
     // 指定哪些域名可以访问此 API  
     w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3333")  

     // 允许访问此 API 的方法  
     w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")  

     // 允许访问此 API 的头部信息  
     w.Header().Set("Access-Control-Allow-Headers", "Accept")  

     // 指定浏览器可以缓存预检请求结果的时间(以秒为单位,这里设置为两小时)  
     w.Header().Set("Access-Control-Max-Age", strconv.Itoa(60*60*2))  
    }

如果我们点击我们所有的按钮,我们会看到获取和删除书籍都有效,但如预期,添加新书不成功:

在预检响应中,Access-Control-Allow-Headers 不包含 content-type 请求头字段。

生产应用使用更多的头部,因此,请仔细检查这些头部以便正确配置CORS设置。

是时候恢复更改并继续我们的列表中的最后一个项目了。

Access-Control-Max-Age: 最大老化时间

如果没有配置,默认值为 0,也就是说浏览器根本不应缓存预检请求的数据。让我们注释掉这行,看看会怎样:

    func enableCors(w http.ResponseWriter) {  
     // 指定可以访问此 API 的方法  
     w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")  

     // 指定可以访问此 API 的请求头  
     w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type")  

     // 指定哪些域名可以访问此 API  
     w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3333")  

     // 指定浏览器缓存预检请求结果的时长(以秒为单位)  
     //w.Header().Set("Access-Control-Max-Age", strconv.Itoa(60*60*2))  
    }

重启应用后,请看看 Network 选项卡:

每次进行 POST 或 DELETE 请求时,浏览器会因为没有缓存策略而发起预检请求。根据规定,GET 请求不会发起预检请求。在测试时,这可以接受,但如果未提供 Access-Control-Max-Age 标头,这会导致服务器不必要的负担。因此建议设置 Access-Control-Max-Age 标头,不过,没有一个理想的值,这要根据你的具体情况和需求来定。

今天大概就说到这吧。你现在应该对CORS有了不错的了解,可以自行深入学习更多了。

接下来去哪里?

正如我在免责声明中提到的,有一篇[Mozilla的优秀长文]讨论了这个主题——现在你们应该已经为此做好了准备。

如果你更喜欢阅读RFC这类文档,这里有一个包含了跨源资源共享(CORS)的内容 — 这个链接指向了CORS部分,如果你有时间和兴趣,也可以读一读全文。

总之,我的时间已经很晚了,所以我得去睡觉了。希望你今天学到了一些新知识,对CORS有了更深的理解,了解它背后的运作机制。下次见!

过得愉快 =)

附:每当我发布新文章时,你将收到一封电子邮件。请点击这里订阅

附注:我最近创建了 一个 Twitter 账号,如果你想让我的时间线不被广告、搞笑视频和马斯克的帖子搞乱,同时也不想错过我的推文,我们可以在那里互相关注 =)

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP