Go语言CORS中间件库jub0bs/cors:更优的跨域资源共享解决方案

jub0bs/cors是一个专为Go语言设计的CORS中间件库,提供更简洁的API、完善的配置验证、调试模式和更强的性能保证,帮助开发者轻松解决跨域资源共享问题。

jub0bs/cors:一个更好的Go语言CORS中间件库

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的文档,你会很快意识到该库提供了不少于四种方式来指定CORS中间件应该允许哪些Web来源:

 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中间件通常必须位于任何身份验证逻辑之前,攻击者甚至不需要进行身份验证。

为了评估影响,我进行了一个测试,其中我同时向一个运行内存限制为50 Mib的Docker化Alertmanager实例发送了几个恶意请求;结果,Docker容器很快耗尽内存并死亡。💀

因为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,而jub0bs/cors的是Config。

这些结构类型字段的名称也不同;下表显示了两个库中对应字段之间的映射:

rs/cors jub0bs/cors
AllowedOrigins Origins
AllowOriginFunc N/A
AllowOriginRequestFunc N/A
AllowOriginV
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计