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/plainmultipart/form-dataapplication/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())

错误信息:

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));

安全建议

✅ 生产环境最佳实践

  1. 不要使用 AllowAllOrigins: true

    // ❌ 不安全
    config.AllowAllOrigins = true
    
    // ✅ 明确指定允许的域名
    config.AllowOrigins = []string{"https://example.com"}
    
  2. 谨慎使用 AllowCredentials

    • 只在需要携带 Cookie 时才设置为 true
    • 不能与 AllowAllOrigins: true 同时使用
  3. 限制允许的方法和头

    // ✅ 只允许必要的方法
    config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"}
    
    // ✅ 只允许必要的头
    config.AllowHeaders = []string{"Content-Type", "Authorization"}
    
  4. 设置合理的缓存时间

    // 预检请求缓存 12 小时,减少请求次数
    config.MaxAge = 12 * time.Hour
    

总结

使用场景 推荐配置
本地开发 cors.Default()AllowAllOrigins: true
测试环境 允许测试域名 + AllowCredentials: true
生产环境 明确指定域名 + 限制方法和头 + 合理的 MaxAge
多子域名 使用 AllowOriginFunc 动态判断

核心要点:

  • CORS 中间件必须放在最前面
  • 生产环境不要使用 AllowAllOrigins: true
  • 需要携带 Cookie 时必须设置 AllowCredentials: true
  • 自定义请求头需要在 AllowHeaders 中声明

参考资源

posted @ 2026-04-28 10:28  牛奔  阅读(83)  评论(0)    收藏  举报