news 2026/8/16 16:07:56

CORS跨域处理与安全头设置:前后端分离的安全基石

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CORS跨域处理与安全头设置:前后端分离的安全基石

# 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文档永远跟代码同步。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/16 16:07:37

Arduino ESP32 安装屡屡失败?这份 6 关闯关清单帮你一次通关

Arduino ESP32 安装屡屡失败?这份 6 关闯关清单帮你一次通关 【免费下载链接】arduino-esp32 Arduino core for the ESP32 family of SoCs 项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32 arduino-esp32 是乐鑫官方为 ESP32 全系芯片提供的…

作者头像 李华
网站建设 2026/8/16 16:04:18

Arduino ESP32 安装失败自救指南:3 个方法一次搞定开发环境

Arduino ESP32 安装失败自救指南:3 个方法一次搞定开发环境 【免费下载链接】arduino-esp32 Arduino core for the ESP32 family of SoCs 项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32 上周有个朋友发我一张照片:崭新的 ESP3…

作者头像 李华
网站建设 2026/8/16 16:02:38

palera1n 越狱工具完整指南:从 checkm8 原理到 A8-A11 设备实战

palera1n 越狱工具完整指南:从 checkm8 原理到 A8-A11 设备实战 【免费下载链接】palera1n Jailbreak for A8 through A11, T2 devices, on iOS/iPadOS/tvOS 15.0, bridgeOS 5.0 and higher. 项目地址: https://gitcode.com/GitHub_Trending/pa/palera1n 你的…

作者头像 李华