更优的Go CORS中间件库:jub0bs/cors全面解析

本文详细介绍了jub0bs/cors这一Go语言的CORS中间件库,对比了其与rs/cors在API设计、配置验证、调试模式和性能方面的优势,包含具体代码示例和迁移指南。

jub0bs/cors: a better CORS middleware library for Go

TL;DR
我刚刚发布了 jub0bs/cors,这是一个新的 Go CORS 中间件库,可能是目前最好的一个。相比更流行的 rs/cors 库,它具有以下优势:

  • 更简单的 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import (
  "io"
  "log"
  "net/http"

  "github.com/jub0bs/cors"
)

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("GET /hello", handleHello) // 这个没有 CORS

  corsMw, err := cors.NewMiddleware(cors.Config{
    Origins:        []string{"https://example.com"},
    Methods:        []string{http.MethodGet, http.MethodPost},
    RequestHeaders: []string{"Authorization"},
  })
  if err != nil {
    log.Fatal(err)
  }
  corsMw.SetDebug(true) // 可选:开启调试模式

  api := http.NewServeMux()
  api.HandleFunc("GET /users", handleUsersGet)
  api.HandleFunc("POST /users", handleUsersPost)
  mux.Handle("/api/", http.StripPrefix("/api", corsMw.Wrap(api)))

  log.Fatal(http.ListenAndServe(":8080", mux))
}

func handleHello(w http.ResponseWriter, _ *http.Request) {
  io.WriteString(w, "Hello, World!")
}

func handleUsersGet(w http.ResponseWriter, _ *http.Request) {
  // 省略
}

func handleUsersPost(w http.ResponseWriter, _ *http.Request) {
  // 省略
}

如果你已经信服并希望立即将代码迁移到 jub0bs/cors,请跳转到本文后面的迁移指南。

为什么你应该选择 jub0bs/cors

rs/cors 作为最流行的 Go CORS 中间件库值得称赞。它的开发仍在进行中,已持续近十年,至今许多开源项目都依赖它。但它完美吗?当然没有库是完美的,但我相信 Go 开发者值得最好的。在我看来,rs/cors 存在一些 jub0bs/cors 解决的缺点;请允许我详细说明其中几个。

更简单的 API

如果你查阅 rs/cors 的文档,你会很快发现该库提供了至少四种方式来指定哪些 Web 源应该被允许的 CORS 中间件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type Options struct {
  AllowedOrigins []string
  AllowOriginFunc func(string) bool
  AllowOriginRequestFunc func(*http.Request, string) bool /* deprecated */
  AllowOriginVaryRequestFunc func(*http.Request, string) (bool, []string)
  AllowedMethods []string
  AllowedHeaders []string
  ExposedHeaders []string
  MaxAge int
  AllowCredentials bool
  AllowPrivateNetwork bool
  OptionsPassthrough bool
  OptionsSuccessStatus int
  Debug bool
  Logger Logger
}

这种冗余选项的激增不仅令人不知所措,而且正如我之前的一篇文章中提到的,其中一些选项很容易被误用。相比之下,jub0bs/cors 提供了一种单一的方式来配置你想要的 CORS 中间件的任何特定方面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type Config struct {
  Origins         []string
  Credentialed    bool
  Methods         []string
  RequestHeaders  []string
  MaxAgeInSeconds int
  ResponseHeaders []string
  ExtraConfig
  // 包含过滤或未导出的字段
}

更晦涩的选项隐藏在一个名为 ExtraConfig 的单独结构类型中。因此,jub0bs/cors 的 API 更容易理解,并且更适合自动完成。

作为额外的好处,与 rs/cors 相反,jub0bs/cors 允许你将整个 CORS 配置与 JSON 或 YAML 相互编组。

更好的文档

我特别小心地为 jub0bs/cors 编写了精确且有用的文档。特别是,最近在 net/http 中添加的增强路由模式值得澄清;正确应用 CORS 中间件(无论是由哪个库产生的)与使用“方法完整”模式结合确实一开始可能具有挑战性。我自己当然也感到困惑,直到 Carlana Johnson 帮助我意识到 http.ServeMux 组合是关键。

为了免去 jub0bs/cors 用户的类似困惑,我在文档中包含了说明性的例子。除此之外,尽管 jub0bs/cors 与 net/http 的路由器配合得最好,我还是在一个单独的 GitHub 仓库中发布了涉及第三方路由器(如 Chi、Echo 和 Fiber)的例子。

广泛的配置验证

在我之前的一篇博客文章和我在 GopherCon Europe 2023 上的演讲中,我认为缺乏配置验证是大多数人难以解决 CORS 错误的主要原因之一。不幸的是,一年后,rs/cors 在这方面没有改进;考虑以下代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import "github.com/rs/cors"

func main() {
  corsMw := cors.New(cors.Options{
    AllowedOrigins: []string{
      "https://example.org",
      "https://*.com",
    },
    AllowedMethods: []string{
      "CONNECT",
      "RÉSUMÉ",
    },
    AllowedHeaders: []string{"auth orization"},
  })
  // 其余部分为简洁省略
}

你能发现所需中间件的 CORS 配置问题吗?也许你可以(经过一些仔细检查),但 rs/cors 本身不能,仅仅因为它几乎不对你的 CORS 配置执行验证。相反,它很乐意产生一个功能失调的中间件(这可能会让你非常沮丧)或不安全的中间件(这会使你的用户面临风险)。

与 rs/cors 不同,jub0bs/cors 执行广泛的配置验证,以防止你创建功能失调或不安全的 CORS 中间件:

 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
import (
  "fmt"
  "os"

  "github.com/jub0bs/cors"
)

func main() {
  corsMw, err := cors.NewMiddleware(cors.Config{
    Origins: []string{
      "https://example.org",
      "https://*.com",
    },
    Methods: []string{
      "CONNECT",
      "RÉSUMÉ",
    },
    RequestHeaders: []string{"auth orization"},
  })
  if err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
  // 其余部分为简洁省略
}

上面的程序失败(正如它应该的那样),并显示一条错误消息,提醒你 CORS 配置中的所有问题:

1
2
3
4
5
cors: forbidden method name "CONNECT"
cors: invalid method name "RÉSUMÉ"
cors: invalid request-header name "auth orization"
cors: for security reasons, origin patterns like "https://*.com" that
  encompass subdomains of a public suffix are by default prohibited

调试模式

大多数 CORS 中间件库倾向于在对失败的预检请求的响应中省略所有 CORS 头。rs/cors 的行为就是这样;jub0bs/cors 默认也是如此。一方面,这种行为遵循了良好的安全实践:CORS 中间件在预检失败时理想情况下应尽可能少地向潜在对手揭示其配置(如允许的源、允许的方法等)。另一方面,正如我之前的一篇文章中解释的,这种行为严重阻碍了 CORS 问题的故障排除:浏览器在预检失败时信息不足,最终引发一个错误消息,掩盖了你的 CORS 问题的根本原因。

通常,你会得到类似以下的错误消息:

1
2
3
4
5
6
Access to fetch at https://your-server.example.com/users
from origin https://your-client.example.com has been blocked by CORS policy:
Response to preflight request doesn’t pass access control check:
No Access-Control-Allow-Origin header is present on the requested resource.
If an opaque response serves your needs, set the request’s mode to no-cors
to fetch the resource with CORS disabled.

困惑之余,你去仔细检查服务器的 CORS 配置,发现 https://your-client.example.com 实际上被列为允许的源……🤔 最后,在几个小时毫无进展之后,你发现了 CORS 问题的根本原因:服务器的配置不够宽松,因为客户端的请求包含一些头(比如 Authorization),这些头恰好没有被明确允许,但应该被允许。🤬

在我看来,CORS 中间件的这种行为是 CORS 错误以难以和耗时故障排除而臭名昭著的主要原因之一。rs/cors 通过让用户在其中间件配置中指定一个记录器来摆脱困境。然后,该记录器为中间件处理的每个请求发出信息性消息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
2024/04/23 13:40:12 Handler: Preflight request
2024/04/23 13:40:12   Preflight aborted: headers '[Authorization]' not allowed
2024/04/23 13:40:13 Handler: Preflight request
2024/04/23 13:40:13   Preflight aborted: method 'PUT' not allowed
2024/04/23 13:40:14 Handler: Preflight request
2024/04/23 13:40:14   Preflight aborted: origin 'https://example.com' not allowed
2024/04/23 13:40:15 Handler: Actual request
2024/04/23 13:40:15   Actual request no headers added: missing origin
2024/04/23 13:40:17 Handler: Actual request
2024/04/23 13:40:17   Actual request no headers added: missing origin
2024/04/23 13:40:18 Handler: Actual request
2024/04/23 13:40:18   Actual response added headers: map[Access-Control-Allow-Origin:[https://example.org] Vary:[Origin]]

这种方法确实简化了故障排除,但远非理想:你可以想象这样的记录器在重负载下的 CORS 感知服务器上会产生多少噪音……🤢 jub0bs/cors 采取了不同的方法:它的 CORS 中间件提供了一个调试模式,你可以通过 (*Middleware).SetDebug 方法切换:

1
2
3
4
5
6
// jub0bs/cors
corsMw, err := cors.NewMiddleware(cors.Config{ /* omitted */ }
if err != nil {
  log.Fatal(err)
}
corsMw.SetDebug(true)

调试模式在开启时,会覆盖上述中间件的行为,并在对预检请求的响应中包含更多信息,即使是失败的请求。开启调试模式本质上将你的 CORS 中间件变成了一个“浏览器耳语者”:通过给浏览器足够的上下文信息,中间件能够从中引发错误消息,你实际上会发现这些消息有助于解决你的 CORS 问题。因此,我强烈鼓励你在面对令人困惑的 CORS 问题时激活此调试模式。

但等等;还有更多!因为 SetDebug 方法是并发安全的,你可以自由地机会性地动态切换调试模式,即使你的服务器正在运行且你的 CORS 中间件正在处理请求(即无需停止服务器、编辑其源代码,然后重新启动服务器)。你所要做的就是以某种方式暴露切换调试模式的能力;例如,我修改了本文顶部的程序,添加了一个 /debug 端点用于切换调试模式:

 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
54
55
56
57
package main

import (
    "io"
    "log"
    "net/http"
    "strconv"

    "github.com/jub0bs/cors"
)

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("GET /hello", handleHello) // 这个没有 CORS

  corsMw, err := cors.NewMiddleware(cors.Config{
    Origins:        []string{"https://example.com"},
    Methods:        []string{http.MethodGet, http.MethodPost},
    RequestHeaders: []string{"Authorization"},
  })
  if err != nil {
    log.Fatal(err)
  }

  setDebug := func(w http.ResponseWriter, r *http.Request) {
    debug, err := strconv.ParseBool(r.URL.Query().Get("debug"))
    if err != nil {
      http.Error(w, "invalid debug value", http.StatusBadRequest)
      return
    }
    corsMw.SetDebug(debug)
  }
  mux.Handle("PUT /debug", authZMiddleware(http.HandlerFunc(setDebug)))

  api := http.NewServeMux()
  api.HandleFunc("GET /users", handleUsersGet)
  api.HandleFunc("POST /users", handleUsersPost)
  mux.Handle("/api/", http.StripPrefix("/api", corsMw.Wrap(api)))

  log.Fatal(http.ListenAndServe(":8080", mux))
}

func handleHello(w http.ResponseWriter, _ *http.Request) {
  io.WriteString(w, "Hello, World!")
}

func authZMiddleware(h http.Handler) http.Handler {
  return h // 实际实现省略
}

func handleUsersGet(w http.ResponseWriter, _ *http.Request) {
  // 省略
}

func handleUsersPost(w http.ResponseWriter, _ *http.Request) {
  // 省略
}

请注意,在实践中,就像你不应该公开暴露你的 pprof 端点一样,你不应该向全世界公开暴露切换此调试模式的能力。因此,在上面的例子中,我将我的 setDebug 处理程序包装在一些授权中间件中。另一种方法是在反向代理级别限制对 /debug 端点的访问。

我《无畏 CORS》设计哲学的细心读者可能会反对 jub0bs/cors 的调试模式似乎违反了原则 11:

保证配置不可变性。

但我会反驳说,激活调试模式(以简化故障排除)只会稍微改变中间件的行为;切换调试模式不会修改允许的源集、允许的方法、允许的请求头、最大年龄等。中间件配置的此类修改仍然需要服务器重启。😇

编辑(2025/06/16):由 jub0bs/cors 产生的 CORS 中间件现在可以动态重新配置(无需服务器重启)并且以并发安全的方式进行。

更强的性能保证

总体而言,jub0bs/cors 和现代版本的 rs/cors 具有相似的性能特征。然而,在一种特定情况下,rs/cors v1.10.1 表现糟糕,甚至可能促进拒绝服务。响应一些伪装成 CORS 预检请求的恶意请求,rs/cors 中间件确实分配了过多的内存,在某些情况下比 jub0bs/cors 中间件多几个数量级:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
goos: darwin
goarch: amd64
pkg: github.com/jub0bs/cors-benchmarks
cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
                │    rs-cors    │              jub0bs-cors  │
                │    sec/op     │   sec/op     vs base      │
malicious_ACRH    17238.0n ± 3%   438.2n ± 5%  -97.46% 😱

                │   rs-cors     │              jub0bs-cors  │
                │     B/op      │     B/op     vs base      │
malicious_ACRH    37832.0 ± 0%     928.0 ± 0%  -97.55% 😱

在我能想到的最坏(但现实)情况下,一个 1 Mib 的恶意请求导致 rs/cors 中间件分配了巨大的 116 MiB!这种行为可能被对手滥用以在服务器的运行时(内存分配器和垃圾收集器)上产生不当负载。当然,这种攻击向量不如 ReDoS 严重,并且大多数 WAF 可能会阻止那些恶意请求,但这仍然应该引起关注。特别是,因为 CORS 中间件通常必须位于任何身份验证逻辑之前,攻击者甚至不需要进行身份验证。

因为 jub0bs/cors 遵循防御性方法,它对此类问题免疫并表现出可预测的性能特征。

偏爱 rs/cors 而非 jub0bs/cors 的原因

尽管 jub0bs/cors 有很多优点,你可能仍然有正当理由坚持使用 rs/cors v1.11.0+,至少目前如此。以下是我能想到的尽可能详尽的列表:

  • 你由于某种原因还不能迁移到 Go v1.22(jub0bs/cors 假设其语义)。
  • 你希望允许方案既不是 http 也不是 https 的 Web 源。
  • 你需要比 jub0bs/cors 支持的更灵活的源模式。
  • 你需要动态修改 CORS 中间件的配置,而无需重启服务器。
  • 你想为 CORS 中间件处理的每个请求记录一个事件。

如果以上项目都不描述你当前的情况,我鼓励你尽快迁移到 jub0bs/cors。😇

迁移指南

如果你准备从 rs/cors 迁移到 jub0bs/cors,我预计你会发现这种迁移很简单。本文的以下小节重点介绍两个库之间的相似之处和差异。如果你在迁移项目时仍然遇到困难,请随时(在 Mastodon 上)向我寻求指导甚至拉取请求。

在下面所有出现名为 handler 的变量的示例中,该变量假定为 http.Handler 类型并在别处声明。

安装 jub0bs/cors

要开始依赖 jub0bs/cors,只需在你的项目中运行以下 shell 命令:

1
go get github.com/jub0bs/cors

一旦你直接依赖 jub0bs/cors 而不再依赖 rs/cors,不要忘记通过运行以下命令来整理你的模块:

1
go mod tidy

配置 CORS 中间件

基本配置结构类型有不同的名称:rs/cors 的是 Options,

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计