
当web应用发起包含自定义http头的跨域请求时,浏览器会首先发送一个“预检(preflight)”options请求。本文将深入探讨这一机制,并通过go语言服务器端的具体案例,演示如何正确处理这些预检请求,以确保带有自定义`authorization`等头的cors get请求能够顺利执行,避免常见的404或cors错误。
理解CORS与预检请求
跨域资源共享(CORS)是一种W3C标准,它允许浏览器向跨源服务器发出XMLHttpRequest请求,从而克服了同源策略的限制。然而,并非所有跨域请求都会直接发送。对于某些“非简单请求”,浏览器会先自动发送一个HTTP OPTIONS请求到目标资源,以确定实际请求是否安全可发送。这个OPTIONS请求被称为“预检请求”(Preflight Request)。
非简单请求的条件通常包括:
- 使用了GET、HEAD、POST以外的HTTP方法(如PUT、DELETE)。
- 发送了浏览器自动设置以外的HTTP头,例如Authorization、X-Custom-Header等自定义头部。
- Content-Type的值不是application/x-www-form-urlencoded、multipart/form-data或text/plain。
当一个AngularJS应用(或其他前端框架)发起一个带有Authorization自定义头的GET请求时,即使GET本身是简单请求方法,但由于Authorization头的存在,该请求会被视为非简单请求,从而触发浏览器发送预检OPTIONS请求。
问题现象:预检OPTIONS请求失败
考虑以下前端AngularJS代码,它尝试向/banks接口发送一个带有Authorization头的GET请求:
$http.get(env.apiURL()+'/banks', {
headers: {
'Authorization': 'Bearer '+localStorageService.get('access_token')
}
})当这个请求被触发时,浏览器实际会先发送一个OPTIONS请求,其结构大致如下:
OPTIONS /banks HTTP/1.1 Host: localhost:8080 Connection: keep-alive Access-Control-Request-Method: GET Origin: http://localhost:8081 Access-Control-Request-Headers: accept, authorization // 注意这里包含了 'authorization' Accept: */* // ... 其他头部
如果服务器端没有明确处理这个OPTIONS请求,或者路由配置中没有对应的OPTIONS处理器,服务器可能会返回一个404 Not Found错误,因为默认情况下它只知道如何处理GET请求。例如,一个Go语言服务器可能只配置了GET路由:
r := mux.NewRouter()
r.HandleFunc("/banks", RetrieveAllBank).Methods("GET")
http.ListenAndServe(":8080", r)在这种情况下,OPTIONS /banks请求将不会匹配到任何路由,导致服务器返回404 Not Found。即使服务器在后续的GET请求中设置了正确的CORS响应头(如Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers),由于预检请求的失败,浏览器会阻止实际的GET请求发出,并在控制台报告CORS错误。
服务器返回的404响应可能看起来像这样,即使其中包含了CORS相关的响应头,也无济于事,因为请求方法不匹配:
HTTP/1.1 404 Not Found Access-Control-Allow-Headers: Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE Access-Control-Allow-Origin: http://localhost:8081 Content-Type: text/plain; charset=utf-8 Date: Mon, 17 Mar 2014 11:05:20 GMT Content-Length: 19
解决方案:服务器端处理OPTIONS预检请求
解决此问题的关键在于服务器端必须明确地处理OPTIONS请求。当服务器收到OPTIONS请求时,它应该检查请求头中的Access-Control-Request-Method和Access-Control-Request-Headers,并用适当的CORS响应头进行回复,告知浏览器允许哪些方法和头部。
以下是Go语言服务器的修改示例,通过实现http.Handler接口来拦截并处理OPTIONS请求:
package main
import (
"fmt"
"github.com/gorilla/mux"
"log"
"net/http"
)
// RetrieveAllBank 模拟实际的GET请求处理器
func RetrieveAllBank(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to the banks API!")
}
// MyServer 结构体,用于包装mux.Router并实现http.Handler接口
type MyServer struct {
r *mux.Router
}
// ServeHTTP 方法实现http.Handler接口
func (s *MyServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// 设置CORS响应头,允许指定来源、方法和头部
if origin := req.Header.Get("Origin"); origin == "http://localhost:8081" {
rw.Header().Set("Access-Control-Allow-Origin", origin)
rw.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
rw.Header().Set("Access-Control-Allow-Headers",
"Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") // 确保包含所有自定义头
rw.Header().Set("Access-Control-Max-Age", "86400") // 预检请求的缓存时间,单位秒
}
// 如果是预检OPTIONS请求,则直接返回,不继续处理路由
if req.Method == "OPTIONS" {
rw.WriteHeader(http.StatusOK) // 返回200 OK
return
}
// 如果不是OPTIONS请求,则交给Gorilla Mux路由器处理
s.r.ServeHTTP(rw, req)
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/banks", RetrieveAllBank).Methods("GET") // 注册GET方法路由
fmt.Println("Server listening on :8080")
// 将自定义的MyServer作为http.ListenAndServe的处理器
log.Fatal(http.ListenAndServe(":8080", &MyServer{r}))
}
代码解析:
- MyServer结构体和ServeHTTP方法: 我们创建了一个MyServer结构体来包装*mux.Router,并为其实现了http.Handler接口的ServeHTTP方法。这样,MyServer就可以作为整个HTTP服务器的入口处理器。
-
设置CORS响应头: 在ServeHTTP方法的开头,我们统一设置了CORS相关的响应头。
- Access-Control-Allow-Origin: 指定允许哪些源(Origin)访问资源。
- Access-Control-Allow-Methods: 指定允许哪些HTTP方法(GET, POST, OPTIONS等)访问资源。务必包含OPTIONS方法。
- Access-Control-Allow-Headers: 这是关键! 它指定了实际请求中允许携带哪些自定义HTTP头。如果客户端发送了Authorization头,这里就必须包含Authorization。
- Access-Control-Max-Age: 可选,用于指定预检请求的缓存时间,避免每次请求都发送预检。
-
处理OPTIONS请求:
if req.Method == "OPTIONS" { rw.WriteHeader(http.StatusOK) // 返回200 OK return }当请求方法是OPTIONS时,我们设置了必要的CORS头后,直接返回http.StatusOK(200 OK),并且不再将请求传递给底层的mux.Router处理。这告诉浏览器,预检成功,实际请求可以安全发送。
- 处理非OPTIONS请求: 如果请求方法不是OPTIONS,则调用s.r.ServeHTTP(rw, req),将请求交给原始的mux.Router进行处理,例如匹配GET /banks路由。
注意事项与最佳实践
- 自定义Header的大小写: 浏览器在发送Access-Control-Request-Headers时,通常会将自定义头名转换为小写(例如Authorization变为authorization)。但是,在服务器端设置Access-Control-Allow-Headers时,推荐使用原始的、大小写敏感的名称(如Authorization),因为有些客户端或服务器实现可能对此敏感,且规范通常使用原始大小写。Go语言的http.Header处理通常是大小写不敏感的,但为了兼容性,保持一致性是好的做法。在本例中,前端发送Authorization,后端Access-Control-Allow-Headers也应包含Authorization。
- *通配符`的使用:**Access-Control-Allow-Origin可以设置为来允许所有来源,但这在生产环境中通常不推荐,因为它会降低安全性。Access-Control-Allow-Headers和Access-Control-Allow-Methods也可以使用`,但同样应谨慎使用,仅在明确了解风险的情况下。
- CORS中间件: 在更复杂的Go应用中,为了避免在每个Handler中重复CORS逻辑,可以考虑使用现有的CORS中间件库,例如github.com/rs/cors,它们提供了更灵活和健壮的CORS配置选项。
- 错误处理: 确保在处理OPTIONS请求时,即使没有匹配到特定路由,也能返回200 OK以及正确的CORS头,而不是404 Not Found。
通过以上修改,前端AngularJS应用发出的带有自定义Authorization头的GET请求,在经过预检OPTIONS请求成功后,将能够顺利到达服务器并获取数据。理解并正确处理CORS预检请求是构建健壮跨域Web应用的关键一步。










