# CORS跨域处理与安全头设置:前后端分离的安全基石
摘要: 本篇从CORS预检请求原理讲起,用Gin实现CORS中间件和常用安全响应头(CSP、HSTS、X-Frame-Options等),分享OPTIONS预检请求被认证中间件拦截导致跨域失效的踩坑经历,对比gin-contrib/cors与手写中间件的取舍。
开篇故事
去年我们组前后端分离重构,前端跑在localhost:3000,后端API在localhost:8080。前端刚调第一个接口就炸了,浏览器控制台报Access-Control-Allow-Origin缺失。前端同事跑过来说你这后端是不是坏了。
我当时对CORS理解不深,以为加个Access-Control-Allow-Origin: *就行。加上之后GET请求通了,但POST请求还是报错。查了半天才知道有预检请求这回事,浏览器对非简单请求会先发一个OPTIONS请求探路,服务端返回允许的方法和头部后才发真正的请求。
我的接口根本没处理OPTIONS方法,预检请求直接404了。前端的POST自然就发不出去。这个坑折腾了我大半天,今天就把CORS和安全头一起聊清楚。
一、CORS原理与预检请求
CORS全叫跨域资源共享。浏览器有个同源策略,协议、域名、端口任一不同就算跨域。跨域请求分两种。
简单请求直接发,浏览器在响应头里找Access-Control-Allow-Origin。非简单请求(比如POST JSON、带自定义头)会先发OPTIONS预检,服务端确认允许后才发真实请求。
// 简单请求: GET, 无自定义头, Content-Type为text/plain // 非简单请求: PUT/DELETE, 或Content-Type为application/json // 非简单请求触发预检, 流程如下: // 1. 浏览器发 OPTIONS 请求 // 2. 服务端返回允许的方法、头部、是否带凭证 // 3. 浏览器检查通过后发真实请求二、Gin CORS中间件
手写一个CORS中间件,处理预检请求和真实请求的响应头。
packagemainimport("net/http""github.com/gin-gonic/gin")// CorsMiddleware CORS跨域中间件funcCorsMiddleware()gin.HandlerFunc{returnfunc(c*gin.Context){// 允许的源, 生产环境用具体域名origin:=c.Request.Header.Get("Origin")allowOrigin:="http://localhost:3000"// 设置CORS响应头c.Header("Access-Control-Allow-Origin",allowOrigin)// 允许携带Cookiec.Header("Access-Control-Allow-Credentials","true")// 允许的请求方法c.Header("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, OPTIONS")// 允许的请求头c.Header("Access-Control-Allow-Headers","Content-Type, Authorization, X-Request-ID")// 预检请求缓存时间, 12小时内不重复预检c.Header("Access-Control-Max-Age","43200")_=origin// 实际项目根据origin白名单动态设置// 预检请求直接返回204ifc.Request.Method==http.MethodOptions{c.AbortWithStatus(http.StatusNoContent)return}c.Next()}}funcmain(){r:=gin.Default()// CORS中间件必须在路由前注册r.Use(CorsMiddleware())// 全局生效r.GET("/api/data",func(c*gin.Context){c.JSON(http.StatusOK,gin.H{"data":"hello"})// GET响应})r.POST("/api/data",func(c*gin.Context){c.JSON(http.StatusOK,gin.H{"message":"created"})// POST响应})r.Run(":8080")// 启动服务}用gin-contrib/cors库可以少写代码,配置更灵活。
import("time""github.com/gin-contrib/cors""github.com/gin-gonic/gin")funcmain(){r:=gin.Default()// 使用gin-contrib/cors库, 配置更简洁r.Use(cors.New(cors.Config{AllowOrigins:[]string{"http://localhost:3000"},AllowMethods:[]string{"GET","POST","PUT","DELETE"},AllowHeaders:[]string{"Content-Type","Authorization"},AllowCredentials:true,MaxAge:12*time.Hour,// 预检缓存}))r.Run(":8080")}三、安全响应头设置
除了CORS,还有一组安全头要加。这些头告诉浏览器执行安全策略,防XSS、防点击劫持、强制HTTPS。
// SecurityHeaders 安全响应头中间件funcSecurityHeaders()gin.HandlerFunc{returnfunc(c*gin.Context){// 防止点击劫持, 禁止页面被iframe嵌套c.Header("X-Frame-Options","DENY")// 防MIME类型嗅探, 浏览器不猜测Content-Typec.Header("X-Content-Type-Options","nosniff")// XSS过滤, 检测到攻击时阻止页面渲染c.Header("X-XSS-Protection","1; mode=block")// 强制HTTPS, 1年内所有请求走HTTPSc.Header("Strict-Transport-Security","max-age=31536000; includeSubDomains")// 内容安全策略, 只允许加载同源资源c.Header("Content-Security-Policy","default-src 'self'")// Referer策略, 只发源不发完整路径c.Header("Referrer-Policy","strict-origin-when-cross-origin")c.Next()}}funcmain(){r:=gin.Default()r.Use(CorsMiddleware())// CORS跨域r.Use(SecurityHeaders())// 安全头中间件// 业务路由, 内联handlerr.GET("/api/data",func(c*gin.Context){c.JSON(http.StatusOK,gin.H{"data":"ok"})// 返回数据})r.Run(":8080")// 启动服务}逐个解释一下这些头的作用。X-Frame-Options设为DENY,别人就没法用iframe把你的页面嵌进去做点击劫持。Strict-Transport-Security让浏览器记住这个域名必须走HTTPS,即使用户输入http也会自动跳https。Content-Security-Policy是最强的一个,default-src 'self'表示只允许加载同源资源,外部CDN、内联脚本全被拦。
四、独家踩坑:预检请求被认证中间件拦截
说一个我踩过的坑。有一次我把CORS中间件和JWT认证中间件都挂上了,结果前端还是报跨域错误。浏览器Network里看到OPTIONS请求返回401。
原因是中间件注册顺序的问题。我的JWT中间件对所有请求做认证检查,包括OPTIONS预检请求。预检请求不带Authorization头,所以直接被认证中间件拦返回401。浏览器拿到401认为预检失败,真实请求就不发了。
// 错误写法: 认证中间件拦截了OPTIONSfuncmain(){r:=gin.Default()r.Use(CorsMiddleware())auth:=r.Group("/api")auth.Use(JWTAuth())// OPTIONS请求也走认证, 返回401auth.GET("/data",func(c*gin.Context){c.JSON(http.StatusOK,gin.H{"data":"ok"})})auth.POST("/data",func(c*gin.Context){c.JSON(http.StatusOK,gin.H{"msg":"created"})})r.Run(":8080")}修复方法是在认证中间件里放行OPTIONS请求,或者在CORS中间件里直接Abort掉OPTIONS。
// 修复: 认证中间件放行OPTIONS预检funcJWTAuth()gin.HandlerFunc{returnfunc(c*gin.Context){// 预检请求直接放行ifc.Request.Method==http.MethodOptions{c.Next()return}// 正常的认证逻辑...c.Next()}}另一个方案更干净,把CORS中间件放在所有路由组之前,在CORS中间件里Abort掉OPTIONS请求。这样预检请求不会进入认证中间件。但要注意Access-Control-Allow-Origin不能用*通配符配合Allow-Credentials: true,浏览器会拒绝。必须用具体的origin或者动态读取请求头里的Origin。
五、对比分析与总结
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 手写CORS中间件 | 完全可控,理解原理 | 配置繁琐,容易漏头 | 学习理解 |
| gin-contrib/cors | 配置简洁,功能完善 | 额外依赖 | 生产推荐 |
| Nginx代理同源 | 后端零改动 | 运维配置 | 已有Nginx |
CORS和安全头是前后端分离项目的安全基石。CORS解决跨域访问,安全头防御XSS和点击劫持。核心原则是CORS中间件最早注册,认证中间件放行OPTIONS预检请求。安全头里CSP最强也最复杂,生产环境建议从宽松策略开始,逐步收紧。
下篇我们聊Swagger/OpenAPI文档自动生成,让API文档永远跟代码同步。