跳转到主要内容

在之前的文章中,我们已经介绍了 CORS 是什么以及修复“没有 'access-control-allow-origin' 标头存在”的反向代理方法,但是当您需要更多灵活性时该怎么办? 本文将指导您完成在后端处理 CORS 的过程。 在这种特殊情况下,我们将为 Golang 网络应用程序实现它。 但是,这种技术适用于任何支持“http.Handler”的 Go HTTP 路由器框架。我们将在本教程中使用 Gorilla/Handlers,因此我们将坚持使用 Gorilla 系列并使用基于 Gorilla/Mux 的示例。

自然,与一般的编程一样,有多种方法可以实现相同的东西,从纯粹的 DIY 方法到预先构建并很好地打包在库中的方法。 如上所述,在本教程中,我们将采用后一种方法。 但是,在我们开始所有这些之前,让我们快速回顾一下我们试图解决的问题。

什么是CORS?

Configuring CORS for Go (Golang) image

跨域资源共享 (CORS) 是一种协议,用于放宽同源策略以允许来自一个 [子] 域 (Origin) 的脚本访问另一个域的资源。它通过与目标资源的标头的预检交换来做到这一点。

当脚本向与其来源不同的 [子] 域发出请求时,浏览器首先向该资源发送一个“OPTIONS”请求,以验证该资源是否正在等待来自外部代码的请求。

OPTIONS 请求带有“Origin”标头,以及有关该请求的一些其他信息(查看 CORS 解释器。目标资源然后验证这些详细信息并(如果有效)使用其自己的一组标头进行响应,描述什么是允许的以及如何long 来缓存预检响应。

在我们的修复“没有 'Access-Control-Allow-Origin' Header Present”一文中,我们使用 Nginx 反向代理和一些 RegEx 进行了验证和响应生成。这虽然是解决问题的良好 DevOps 解决方案,但缺乏一定程度的灵活性,并且严重依赖于我们的 RegEx 是否安全。我们在那篇文章中介绍的这种反向代理方法是一种非常好的权宜之计,因为它易于设置且无需更改代码。然而,它确实有一些明显的缺点。

其中最大的是处于该方法中心的 RegEx。

正则表达式的风险


大量 CORS 漏洞是由此类反向代理配置中配置错误的 RegEx 搜索字符串引起的。

例如,RegEx 字符串 `^https\:\/\/.*example\.com$` 乍一看似乎是一个有效的解决方案,它允许我们让来自 example.com 的任何子域的脚本通过 HTTPS 联系我们的 API .它确实适用于这种情况,因为 www.example.com 和 blog.example.com 都会满足这个 RegEx。

然而,很多人错过的是,它实际上也接受以 `https://` 开头并以 `example.com 结尾的任何内容。因此,例如,即使 `www.evilexample.com` 也是一个完全有效的来源然后根据 RegEx 搜索字符串。

显然,我们想要一个同样灵活(如果不是更灵活)同时具有较低配置错误风险的解决方案。

基于代码的解决方案


然后,我们的下一个停靠点是开始考虑以尽可能少的代码更改来实现它。幸运的是,每个生产就绪的 Web 服务器框架都有一定程度的 CORS 支持。在之前的文章中,我们介绍了一种 Django 方法,因此如果 Python 是您的语言,那么绝对值得一读。

在本教程中,我们正在寻找 Go 的解决方案。

教学资源


在本教程的其余部分,我们将扩展以下基于 Gorilla/Mux 的服务器存根:

go
func newREST() *mux.Router {
    r := mux.NewRouter()
    r.HandleFunc("/document", restCreateDocument).Methods("POST")
    return r
}

func main() {
    router := newREST()
    log.Fatal(http.ListenAndServe(":5000", router))
}

这里我们假设资源托管在 api.example.com 并且请求来自 www.example.com。 `https://api.example.com/document` 的路由接受一个 `POST` 请求,它需要一个 JSON blob(文档)和一些身份验证 Cookie。

为了简单起见,我们假设有一些处理程序 `restCreateDocument` 可以适当地验证 cookie 并接受文档。实际上,我们需要一个单独的中间件来处理 cookie 验证,以便我们可以确保所有受保护的端点都得到正确处理,但我们将在本教程中忽略这一点。

添加基本​​ CORS 支持


为了开始使用 CORS 支持,我们将使用 `github.com/gorilla/handlers` 库,因为它提供了一个非常简单的界面。为此,我们将扩展我们的 `main()` 函数,如下所示:

go
func main() {
   router := newREST()
   credentials := handlers.AllowCredentials()
   methods := handlers.AllowedMethods([]string{"POST"})
   ttl := handlers.MaxAge(3600)
   origins := handlers.AllowedOrigins([]string{"www.example.com"})
   log.Fatal(http.ListenAndServe(":5000", handlers.CORS(credentials, methods, origins)(router)))
}


所以,让我们分解一下这里发生了什么。

证书


首先,我们通过以下方式实例化了允许我们的凭证 (Cookie) 的选项:

go
credentials := handlers.AllowCredentials()


这可能是最简单的选项,因为它只是将 `Access-Control-Allow-Credentials: true` 标头添加到 HTTP 响应中。

访问控制允许方法


我们实例化的第二个选项,`AllowedMethods`

go
methods := handlers.AllowedMethods([]string{"POST"})


验证传入的 `Access-Control-Request-Method` 标头的值是否在初始化期间提供给它的支持方法列表中。

访问控制最大年龄(Access-Control-Max-Age)


第三个选项“MaxAge”,设置浏览器缓存选项响应的 TTL(以秒为单位)。

go
   ttl := handlers.MaxAge(3600)


访问控制允许的来源

我们添加了“Allowed-Origins”的最后一个选项

go
origins := handlers.AllowedOrigins([]string{"https://www.example.com"})


验证传入的 `Origin` 标头是否与初始化期间提供给它的源字符串之一匹配(注意:这取决于协议,这意味着如果您的脚本来自 https://www.example.com,那么您需要列出. 省略协议将导致拒绝)。

监听并服务


最后,我们修改了我们的 `http.ListenAndServe` 调用,用我们的中间件引擎 `handlers.CORS` 包装了`router` 对象:

`go
handlers.CORS(credentials, methods, origins)(router)


这就是这里的繁重工作,拦截我们传入的查询以验证任何预检 OPTIONS 请求并在将请求传递给我们的路由器之前验证 CORS 标头。

走得更远


到目前为止,我们已经将类似的结果复制到我们的 Nginx 反向代理解决方案中,这很好,但我们正在编写代码是有原因的。我们希望能够将新的 Origin 动态添加到我们支持的“Access-Control-Allow-Origin”响应列表中(可能使用数据库)。

那么我们如何让它变得动态呢?


好吧,我们可以从头开始编写自己的包装器函数(Gorilla/Handlers 框架非常简单,所以按照我们的意愿弯曲分支并不太难),但在这种特殊情况下,库开发人员比我们领先一步,并且拥有提供了一个动态选项“AllowedOriginValidator(fn OriginValidator)”。

要使用此选项,我们只需编写自己的与签名匹配的函数:

go
type OriginValidator func(string) bool


这可能看起来像这样:

go
func originValidator(origin string) bool {
   valid := false
   err := pool.QueryRow("SELECT IF(origin=?, True, False) as 'valid' FROM origins", origin).Scan(&valid)
   if err != nil { return false }
   return valid
}


在这里,我们的示例 `originValidator` 使用 SQL 将传入的来源与我们的数据库进行比较,如果找到,则返回 `true`,如果未找到,则该函数将返回 `false`。

接下来我们需要做的就是用新的验证器简单地替换我们的 `AllowedOrigins([]string{"www.example.com"})` 选项。

go
origins := handlers.AllowedOriginValidator(originValidator)


那么这是如何工作的呢?


在幕后,主包装器使用我们的函数来验证 Origin 标头是否可以接受。然后,如果它通过它,包装器会将接收到的来源反映到 `access-control-allow-origin` 字段中。

这意味着我们的自定义 `originValidator` 代码经过良好测试并安全地处理任何潜在异常非常重要。在这种代码中,故障安全总是更好,因为拒绝可能有效的请求总是比接受可能无效的请求更可取。

警告和总结


使用 Gorilla/Handlers 的最大警告是缺少选项定义与将其设置为 `*` 通配符相同,这意味着如果您忘记在 `handlers.CORS` 包装器中包含源选项,那么您只需打开自己直到痛苦的世界。

如果要批评这个设计得非常好的库,那就是库的开发人员做出的默认开放设计决策。这种设计本身就可能导致许多意外的错误配置。如果他们采用默认的封闭设计,那么这个库可能是现有 CORS 中间件的最佳实现之一。

Gorilla Web Toolkit 团队的其他出色库包括他们的 CSRF、Sessions 和 Schema Middlewares。每一项都使开发安全的 Web 应用程序和 API 服务变得更加简单。因此,如果您认为自己是安全地鼠,那么您绝对应该检查一下。

延伸阅读


如果您渴望了解更多 CORS 知识,请查看我们的“什么是 CORS?” 解释器文章,或我们的修复“没有 'Access-Control-Allow-Origin' Header Present”文章。

如果您正在寻找其他语言的教程,请查看以下内容:

文章链接