一个更好的 Go CORS 中间件库 :: jub0bs.com

长话短说

我刚刚发布了 jub0bs/cors,一个新的 跨域资源共享 中间件库 ,也许是迄今为止最好的一个。 与更流行的产品相比,它有一些优点 RS/CORS 图书馆,包括

这是客户端代码的代表性示例:

package main

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

  "github.com/jub0bs/cors"
)

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("GET /hello", handleHello) // no CORS on this

  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) // optional: turn debug mode on

  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) {
  // omitted
}

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

如果您已经确信并希望将代码迁移到
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
  // contains filtered or unexported fields
}

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


作为奖励,并且 与 rs/cors 相反,
jub0bs/cors 允许您将整个 CORS 配置编组到 JSON 或 YAML,或者从 JSON 或 YAML 编组/取消编组。


更好的文档

我特别用心去写 准确且有用的文档
为了 jub0bs/cors。 尤其, 最近添加的增强型路由模式
网络/http 值得澄清; 正确应用 CORS 中间件(无论哪个库生成它)并结合使用 “方法齐全”模式 一开始确实很有挑战性。 我自己当然也很困惑,直到 卡拉娜·约翰逊
帮助我意识到http.ServeMux 成分是关键。 为了避免用户 jub0bs/cors 类似的困惑,我已经包括在内 举例说明 在文档中。

最重要的是,尽管 jub0bs/cors 玩得最好
网络/http的路由器,我已经发布了涉及第三方路由器的示例(例如 , 回声, 和 纤维) 在 一个单独的 GitHub 存储库

广泛的配置验证

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

import "github.com/rs/cors"

func main() {
  corsMw := cors.New(cors.Options{
    AllowedOrigins: []string{
      "https://example.org",
      ",
    },
    AllowedMethods: []string{
      "CONNECT",
      "RÉSUMÉ",
    },
    AllowedHeaders: []string{"auth orization"},
  })
  // rest omitted for brevity
}

您能发现所需中间件的 CORS 配置存在问题吗? 也许你可以(经过一些审查)但是 RS/CORS 它本身不能,只是因为它几乎不执行 CORS 配置的验证。 相反,它非常乐意生产功能失调的中间件(这可能会给您带来很大的挫败感)或不安全的中间件(这将使您的用户面临风险)。

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

import (
  "fmt"
  "os"

  "github.com/jub0bs/cors"
)

func main() {
  corsMw, err := cors.NewMiddleware(cors.Config{
    Origins: []string{
      "https://example.org",
      ",
    },
    Methods: []string{
      "CONNECT",
      "RÉSUMÉ",
    },
    RequestHeaders: []string{"auth orization"},
  })
  if err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
  // rest omitted for brevity
}

上面的程序失败(理应如此),并显示一条错误消息,提醒您 CORS 配置的所有问题:

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 " that
  encompass subdomains of a public suffix are by default prohibited

调试模式

大多数 CORS 中间件库倾向于忽略所有 CORS 标头来响应失败的预检请求。
RS/CORS 行为如下; 和 jub0bs/cors 至少在默认情况下也是如此。

一方面,这种行为遵循良好的安全实践:CORS 中间件理想情况下应尽可能少地透露其配置(例如 允许的来源,
允许的方法等)在预检失败时向潜在对手发送信息。 另一方面,正如之前的文章中所解释的,这种行为严重阻碍了 CORS 问题的故障排除:浏览器没有足够的有关预检失败的信息,最终会引发一个错误,其消息掩盖了 CORS 问题的根本原因。


通常,您会收到如下错误消息:

访问获取地址
从原点 已被 CORS 策略阻止:对预检请求的响应未通过访问控制检查:否 Access-Control-Allow-Origin 标头存在于所请求的资源上。 如果不透明响应满足您的需求,请将请求的模式设置为 no-cors
在禁用 CORS 的情况下获取资源。

很困惑,你去仔细检查服务器的 CORS 配置,你发现 事实上,那里被列为允许的来源……🤔

最后,经过几个小时毫无进展,您发现了 CORS 问题的根本原因:服务器的配置不够宽松,因为客户端的请求包含一些标头(Authorization,比如说)这恰好没有被明确允许,但应该是。 🤬


在我看来,CORS 中间件的这种行为是 CORS 错误因故障排除困难且耗时而臭名昭著的主要原因之一。

RS/CORS 摆脱困难 让用户指定一个记录器作为其中间件配置的一部分。 然后,该记录器为中间件处理的每个请求发出一条信息性消息:

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 ' 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:[ 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) // no CORS on this

  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 nil // actual implementation omitted
}

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

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

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


细心的读者可能会反对我的 Fearless CORS 设计理念 jub0bs/cors的调试模式似乎违反了原则11:

保证配置的不变性。

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


更强的性能保证

全面的, jub0bs/cors 和现代版本的 RS/CORS
相似的性能特征。 然而,有一种特定情况 RS/CORS v1.10.1 的表现很糟糕,以至于达到了便利的程度 拒绝服务。 针对一些冒充的恶意请求
CORS-预检请求,
RS/CORS 中间件确实分配了大量的内存,数量级超过 jub0bs/cors 某些情况下的中间件:

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!

攻击者可能会滥用此行为,在服务器运行时(内存分配器和垃圾收集器)上产生过度负载。 当然,这种攻击媒介并不像以下那样严重: 重做服务,大多数 WAF 可能会阻止这些恶意请求,但这仍然应该引起关注。 特别是,由于 CORS 中间件通常必须位于任何身份验证逻辑之前,因此攻击者甚至不需要进行身份验证。

发现这个问题后 RS/CORS v1.10.1,我赶紧打开 GitHub 问题 #170
并发送了修复 拉取请求 #171。 我的拉取请求最终被合并并生成了一个新版本(v1.11.0)已发布,但仅在我提交问题一个月后。 无论漏洞的实际严重程度如何,维护人员对问题长期不解决的容忍度令人担忧。 😟

许多开源项目依赖于 RS/CORS v1.10.1(甚至更旧的版本)可能会受到影响 问题 #170。 其中之一, 普罗米修斯警报管理器,被宣传为一个正常运行的程序
需要不超过 50 Mib 的内存。 为了评估影响,我进行了一项测试,其中我同时向运行有以下命令的 Alertmanager 的 Dockerized 实例发送了几个恶意请求: 内存限制 50 Mib; 结果,Docker 容器很快就耗尽了内存并死掉了。 💀

因为 jub0bs/cors 由于采用防御性方法,因此不会受到此类问题的影响,并且表现出可预测的性能特征。

相比 jub0bs/cors 更青睐 rs/cors 的原因

尽管所有的 jub0bs/cors天哪,你可能还有坚持下去的正当理由 RS/CORS v1.11.0+,至少目前是这样。 这是我能列出的最详尽的清单:

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

如果这些项目都不能描述您目前的情况,我鼓励您迁移到 jub0bs/cors 尽快地。 😇

迁移指南

如果您准备好从 RS/CORSjub0bs/cors,我希望您会发现这种迁移很简单。 本文的以下小节重点介绍了两个库之间的相似点和差异。 如果您仍然难以迁移项目,请随时询问我(在 乳齿象)寻求指导甚至拉取请求。

在下面的所有示例中,变量名为 handler 出现时,该变量被假定为类型 http.Handler

并在别处声明。

安装 jub0bs/cors

开始取决于 jub0bs/cors
只需在项目中运行以下 shell 命令:

go get github.com/jub0bs/cors

一旦你直接依赖 jub0bs/cors
并且不再开启 RS/CORS,不要忘记通过运行以下命令来整理您的模块:

配置 CORS 中间件

基本配置结构类型具有不同的名称:
RS/CORSOptions, 然而 jub0bs/cors
Config。 这些结构类型字段的名称也不同; 两个库中对应字段的映射关系如下表所示:

RS/CORS jub0bs/cors
AllowedOrigins Origins
AllowOriginFunc 不适用
AllowOriginRequestFunc 不适用
AllowOriginVaryRequestFunc 不适用
AllowedMethods Methods
AllowedHeaders RequestHeaders
ExposedHeaders ResponseHeaders
MaxAge MaxAgeInSeconds
AllowedCredentials Credentialed
AllowPrivateNetwork ExtraConfig.PrivateNetworkAccess
OptionsPassthrough 不适用
OptionsSuccessStatus ExtraConfig.PreflightSuccessStatus
Debug N/A(但请参阅调试模式)
Logger 不适用

而且, jub0bs/corsExtraConfig 结构类型
允许您解锁上表中未提及的其他选项。

创建中间件并将其应用于处理程序

生成 CORS 中间件的函数也有不同的名称:
RS/CORS的名称为 New, 然而 jub0bs/cors的名称为 NewMiddleware

除了, jub0bs/corsNewMiddleware 返回的,不仅仅是一个CORS中间件,更是一个 error。 请检查一下 error 结果; 仅当其值为非nil
您可以假设生成的中间件是可用的吗?

最后,将中间件应用于
http.Handler 被(令人困惑地)命名 Handler

RS/CORS,等效方法命名为 Wrap

jub0bs/cors

// jub0bs/cors
corsMw, err := cors.NewMiddleware(cors.Config{ /* omitted */ })
if err != nil {
  // corsMw is unusable; bail out.
  log.Fatal(err)
}
handler = corsMw.Wrap(handler) // wrap corsMw around handler.

默认配置

相比之下 RS/CORS,并且出于其他地方解释的充分理由,
jub0bs/cors 不提供默认的 CORS 配置。 如果要迁移的代码依赖于 RS/CORSDefault

或者 AllowAll 函数,下面的代码片段说明了如何调整您的代码以迁移到 jub0bs/cors

// rs/cors
handler = cors.Default().Handler(handler)

相当于

// jub0bs/cors
corsMw, err := cors.NewMiddleware(cors.Config{
  Origins: []string{"*"},
  Methods: []string{
    http.MethodGet,
    http.MethodHead,
    http.MethodPost,
  },
  RequestHeaders: []string{
    "Accept",
    "Content-Type",
    "X-Requested-With",
  },
})
if err != nil {
  // omitted: bail out, somehow
}
handler = corsMw.Wrap(handler)

// rs/cors
handler = cors.AllowAll().Handler(handler)

相当于

// jub0bs/cors
corsMw, err := cors.NewMiddleware(cors.Config{
  Origins: []string{"*"},
  Methods: []string{
    http.MethodHead,
    http.MethodGet,
    http.MethodPost,
    http.MethodPut,
    http.MethodPatch,
    http.MethodDelete,
  },
  RequestHeaders: []string{"*"},
})
if err != nil {
  // omitted: bail out, somehow
}
handler = corsMw.Wrap(handler)

关于 jub0bs/cors 的起源

对其前身的反思

大约一年前,在意识到开发人员的麻烦之后
跨域资源共享 (CORS) 我发布的主要可以归咎于工具而不是其用户 jub0bs/fcors,Fearless CORS 的参考实现,我的 CORS 中间件库的设计理念,

就个人而言,这个原始库被证明是一个思想的形成实验室,不仅涉及库设计,还涉及算法和数据结构(尤其是 基数树):

然而,尽管 jub0bs/fcors 获得好评 开发商, OWASP, 还有一些 WHATWG 各位(在私人交流中),令人失望的是,它的采用仍然有限。 例如,在撰写本文时,该项目的 GitHub 存储库仅累积了
区区 79 个观星者; 没有什么可嘲笑的,但还远未取得巨大的成功。 相比下, RS/CORS
拥有 超过 2,500 名观星者

如果被迫推测原因 jub0bs/fcors由于采用率低迷,我首先认为,一个有竞争力的软件库,无论其优点如何,不太可能迅速上升到与现有库一样高的受欢迎程度。 其次,我会引用一些有争议的设计决策 jub0bs/fcors。 该库确实严重依赖 功能选项,Go 社区中备受诟病的模式。 赢得这种模式的坚定反对者总是一场艰苦的战斗,而且
我在 2023 年 GopherCon Europe 上就该主题发表的演讲虽然很受欢迎,但并没有影响他们。 在过去的几个月里,我自己对这种模式的看法有所改变。 我仍然相信它在某些情况下很有用,但它的一些痛点对我来说变得更加明显。

与其让我泄气,不如 jub0bs/fcors不引人注目的采用促使我编写了一个精神继承者:一个新的 Go CORS 库,它遵循 Fearless CORS 的原则,但其更传统的 API 具有普遍吸引力的承诺: jub0bs/cors

为什么不为 rs/cors 做出贡献呢?

最后,我应该解决房间里的大象:为什么要创建一个竞争库? 为什么不贡献改进 RS/CORS 反而? 答案并不像看起来那么简单。

虽然我能找出设计上的错误 jub0bs/fcors,我几乎不会改变我的 Fearless CORS 设计理念。 我仍然坚信它的 12 条原则是合理的,值得传播到其他 CORS 库(甚至是 Go 生态系统之外的库)。 怀着这个雄心壮志,我​​确实做出了贡献 问题
拉取请求RS/CORS,其中大部分 奥利维尔·普瓦特雷,图书馆的维护者,请修复或合并。 此外,还有 毫无疑问jub0bs/fcors
激发奥利维尔改进的方面 RS/CORS的表现。

然而,并没有掌舵 RS/CORS,我对它的发展影响力有限; 事实上,这个库与 Fearless CORS 所传达的理想,即易于使用且不易误用的 CORS 库还有很大差距。 例如,多个钩子(例如 AllowOriginFunc) 那
RS/CORS 在我看来,这些规定是危险的错误特征。 如何改进库的这方面并不明显; 事实上,还有一个这样的钩子 最近潜入了 API。 弃用所有这些钩子将是一个很好的第一步,但完全删除对它们的支持将构成重大变化。 也许是一个假设 v2RS/CORS
可以使该库与 Fearless CORS 保持一致,但我不清楚 Olivier 是否计划在不久的将来发布新的主要版本。

如果我不能弯曲 RS/CORS 变成接近我理想的 CORS 中间件库的形状,我至少可以尝试用更好的库取代它。 这是我的志向 jub0bs/cors

致谢

谢谢 卡拉娜·约翰逊迈克·斯蒂芬 感谢您花时间审阅这篇文章的初稿。

Leave a Reply

Your email address will not be published. Required fields are marked *

近期新闻​

编辑精选​