Gin 框架中的 CORS 跨域处理完全指南
什么是 CORS?
想象一下这个场景:
- 你的前端网站运行在
http://localhost:3000 - 你的后端 API 运行在
http://localhost:8080 - 当前端尝试调用后端 API 时,浏览器突然报错:
Access to fetch at 'http://localhost:8080/api/users' from origin 'http://localhost:3000' has been blocked by CORS policy
这就是跨域问题!
为什么会有跨域限制?
浏览器有一个安全机制叫同源策略(Same-Origin Policy):
- 同源:协议、域名、端口三者完全相同
- 跨域:只要有一个不同就算跨域
| 当前页面 | 请求地址 | 是否跨域 | 原因 |
|---|---|---|---|
http://localhost:3000 |
http://localhost:8080 |
✅ 跨域 | 端口不同 |
http://example.com |
https://example.com |
✅ 跨域 | 协议不同 |
http://a.example.com |
http://b.example.com |
✅ 跨域 | 子域名不同 |
http://example.com/page1 |
http://example.com/page2 |
❌ 不跨域 | 完全相同 |
浏览器默认会阻止跨域请求,防止恶意网站窃取数据。
CORS 是什么?
CORS(Cross-Origin Resource Sharing,跨域资源共享)是一种机制,让服务器通过设置特定的 HTTP 响应头,告诉浏览器:"这个跨域请求是安全的,允许它通过"。
安装 gin-contrib/cors
go get github.com/gin-contrib/cors
基础用法
1️⃣ 最简单的配置:允许所有跨域请求
package main
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
// 使用默认配置
router.Use(cors.Default())
router.GET("/api/hello", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Hello World"})
})
router.Run(":8080")
}
cors.Default() 的默认行为:
- 允许所有来源(
*) - 允许 GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS 方法
- 允许 Origin、Content-Length、Content-Type 请求头
2️⃣ 自定义配置:精细控制
package main
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"time"
)
func main() {
router := gin.Default()
// 自定义 CORS 配置
config := cors.Config{
AllowOrigins: []string{"http://localhost:3000", "https://example.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}
router.Use(cors.New(config))
router.GET("/api/users", func(c *gin.Context) {
c.JSON(200, gin.H{"users": []string{"Alice", "Bob"}})
})
router.Run(":8080")
}
配置参数详解
核心参数
| 参数 | 类型 | 说明 | 示例 |
|---|---|---|---|
AllowOrigins |
[]string |
允许的来源列表 | []string{"http://localhost:3000"} |
AllowAllOrigins |
bool |
是否允许所有来源(设为 true 时会忽略 AllowOrigins) | true |
AllowMethods |
[]string |
允许的 HTTP 方法 | []string{"GET", "POST"} |
AllowHeaders |
[]string |
允许的请求头 | []string{"Content-Type", "Authorization"} |
AllowCredentials |
bool |
是否允许携带凭证(cookies、HTTP 认证) | true |
ExposeHeaders |
[]string |
允许前端访问的响应头 | []string{"X-Total-Count"} |
MaxAge |
time.Duration |
预检请求的缓存时间 | 12 * time.Hour |
实际应用示例
场景 1:开发环境 - 允许所有来源
func CORSMiddleware() gin.HandlerFunc {
config := cors.DefaultConfig()
config.AllowAllOrigins = true
config.AllowCredentials = true
config.AllowHeaders = []string{"*"}
return cors.New(config)
}
router.Use(CORSMiddleware())
适用场景: 本地开发、测试环境
场景 2:生产环境 - 只允许特定域名
func CORSMiddleware() gin.HandlerFunc {
config := cors.Config{
AllowOrigins: []string{
"https://www.example.com",
"https://app.example.com",
},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "X-Requested-With"},
ExposeHeaders: []string{"Content-Length", "X-Total-Count"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}
return cors.New(config)
}
router.Use(CORSMiddleware())
适用场景: 生产环境,安全性要求高
场景 3:动态判断来源(支持多个子域名)
func CORSMiddleware() gin.HandlerFunc {
config := cors.Config{
AllowOriginFunc: func(origin string) bool {
// 允许所有 example.com 的子域名
return strings.HasSuffix(origin, ".example.com") ||
origin == "https://example.com"
},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}
return cors.New(config)
}
router.Use(CORSMiddleware())
适用场景: 需要支持多个子域名的 SaaS 应用
CORS 工作原理
简单请求 vs 预检请求
简单请求(Simple Request)
满足以下条件的请求会被视为简单请求:
- 方法是 GET、HEAD 或 POST
- 请求头只包含简单头(Accept、Accept-Language、Content-Language、Content-Type 等)
- Content-Type 只能是
text/plain、multipart/form-data或application/x-www-form-urlencoded
流程:
浏览器 → 直接发送请求 → 服务器
服务器 → 返回响应 + CORS 头 → 浏览器检查 CORS 头
预检请求(Preflight Request)
不满足简单请求条件的请求(比如使用 PUT、DELETE 方法,或自定义请求头)会先发送预检请求。
流程:
1. 浏览器 → 发送 OPTIONS 请求(预检) → 服务器
2. 服务器 → 返回允许的方法和头 → 浏览器
3. 浏览器检查通过 → 发送真正的请求 → 服务器
4. 服务器 → 返回响应 → 浏览器
示例:
# 预检请求
OPTIONS /api/users HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization
# 服务器响应
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 86400
常见问题与解决方案
❌ 问题 1:设置了 CORS 但还是报错
错误信息:
Access to fetch at 'http://localhost:8080/api/users' from origin 'http://localhost:3000'
has been blocked by CORS policy: Response to preflight request doesn't pass access control check
原因: CORS 中间件没有放在最前面,被其他中间件(如认证中间件)拦截了。
解决方案:
router := gin.Default()
// ✅ CORS 必须放在第一个
router.Use(cors.Default())
// 其他中间件放在后面
router.Use(AuthMiddleware())
router.Use(LoggerMiddleware())
❌ 问题 2:携带 Cookie 的请求失败
错误信息:
Access to fetch at '...' has been blocked by CORS policy:
The value of the 'Access-Control-Allow-Credentials' header in the response is ''
which must be 'true' when the request's credentials mode is 'include'
原因: 前端使用了 credentials: 'include',但后端没有设置 AllowCredentials: true。
解决方案:
config := cors.Config{
AllowOrigins: []string{"http://localhost:3000"},
AllowCredentials: true, // ✅ 必须设置为 true
AllowHeaders: []string{"Content-Type", "Authorization"},
}
router.Use(cors.New(config))
前端代码:
fetch('http://localhost:8080/api/users', {
credentials: 'include', // 携带 Cookie
headers: {
'Content-Type': 'application/json'
}
})
❌ 问题 3:自定义请求头被拒绝
错误信息:
Request header field x-custom-header is not allowed by Access-Control-Allow-Headers
解决方案:
config := cors.Config{
AllowOrigins: []string{"http://localhost:3000"},
AllowHeaders: []string{
"Content-Type",
"Authorization",
"X-Custom-Header", // ✅ 添加自定义头
},
}
router.Use(cors.New(config))
或者直接允许所有头:
config.AllowHeaders = []string{"*"}
实战案例:完整的 API 服务器
package main
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
func main() {
router := gin.Default()
// CORS 配置
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:3000"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length", "X-Total-Count"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
// 公开 API(不需要认证)
router.GET("/api/public/status", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
// 需要认证的 API
authorized := router.Group("/api")
authorized.Use(AuthMiddleware())
{
authorized.GET("/users", getUsers)
authorized.POST("/users", createUser)
authorized.PUT("/users/:id", updateUser)
authorized.DELETE("/users/:id", deleteUser)
}
router.Run(":8080")
}
// 简单的认证中间件示例
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
c.Abort()
return
}
c.Next()
}
}
func getUsers(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"users": []string{"Alice", "Bob"}})
}
func createUser(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{"message": "用户创建成功"})
}
func updateUser(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"message": "用户 " + id + " 更新成功"})
}
func deleteUser(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"message": "用户 " + id + " 删除成功"})
}
前端调用示例
JavaScript Fetch API
// GET 请求
fetch('http://localhost:8080/api/users', {
method: 'GET',
credentials: 'include', // 携带 Cookie
headers: {
'Authorization': 'Bearer your-token-here'
}
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// POST 请求
fetch('http://localhost:8080/api/users', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token-here'
},
body: JSON.stringify({
name: 'Charlie',
email: 'charlie@example.com'
})
})
.then(response => response.json())
.then(data => console.log(data));
Axios
import axios from 'axios';
// 配置 Axios 实例
const api = axios.create({
baseURL: 'http://localhost:8080',
withCredentials: true, // 携带 Cookie
headers: {
'Content-Type': 'application/json'
}
});
// 添加请求拦截器(自动添加 Token)
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 使用
api.get('/api/users')
.then(response => console.log(response.data))
.catch(error => console.error(error));
api.post('/api/users', { name: 'David', email: 'david@example.com' })
.then(response => console.log(response.data));
安全建议
✅ 生产环境最佳实践
-
不要使用
AllowAllOrigins: true// ❌ 不安全 config.AllowAllOrigins = true // ✅ 明确指定允许的域名 config.AllowOrigins = []string{"https://example.com"} -
谨慎使用
AllowCredentials- 只在需要携带 Cookie 时才设置为
true - 不能与
AllowAllOrigins: true同时使用
- 只在需要携带 Cookie 时才设置为
-
限制允许的方法和头
// ✅ 只允许必要的方法 config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"} // ✅ 只允许必要的头 config.AllowHeaders = []string{"Content-Type", "Authorization"} -
设置合理的缓存时间
// 预检请求缓存 12 小时,减少请求次数 config.MaxAge = 12 * time.Hour
总结
| 使用场景 | 推荐配置 |
|---|---|
| 本地开发 | cors.Default() 或 AllowAllOrigins: true |
| 测试环境 | 允许测试域名 + AllowCredentials: true |
| 生产环境 | 明确指定域名 + 限制方法和头 + 合理的 MaxAge |
| 多子域名 | 使用 AllowOriginFunc 动态判断 |
核心要点:
- CORS 中间件必须放在最前面
- 生产环境不要使用
AllowAllOrigins: true - 需要携带 Cookie 时必须设置
AllowCredentials: true - 自定义请求头需要在
AllowHeaders中声明

浙公网安备 33010602011771号