CORS跨域 #

一、CORS概述 #

1.1 什么是跨域 #

跨域是指浏览器出于安全考虑,限制从一个源访问另一个源的资源:

text
同源策略:协议 + 域名 + 端口 必须相同

示例:
http://localhost:3000 → http://localhost:8080  跨域(端口不同)
http://example.com    → https://example.com    跨域(协议不同)
http://a.example.com  → http://b.example.com   跨域(域名不同)

1.2 CORS原理 #

CORS(Cross-Origin Resource Sharing)是一种允许跨域请求的机制:

text
浏览器 → OPTIONS预检请求 → 服务器返回允许的源
浏览器 → 实际请求 → 服务器返回数据

1.3 相关响应头 #

响应头 说明
Access-Control-Allow-Origin 允许的源
Access-Control-Allow-Methods 允许的方法
Access-Control-Allow-Headers 允许的请求头
Access-Control-Allow-Credentials 允许携带凭证
Access-Control-Max-Age 预检请求缓存时间

二、基本CORS配置 #

2.1 简单CORS中间件 #

go
func CORSMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
        
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }
        
        c.Next()
    }
}

func main() {
    r := gin.Default()
    r.Use(CORSMiddleware())
    r.Run()
}

2.2 指定允许的源 #

go
func CORSMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        origin := c.GetHeader("Origin")
        
        // 允许的源列表
        allowedOrigins := []string{
            "http://localhost:3000",
            "https://example.com",
        }
        
        allowed := false
        for _, o := range allowedOrigins {
            if origin == o {
                allowed = true
                break
            }
        }
        
        if allowed {
            c.Header("Access-Control-Allow-Origin", origin)
        }
        
        c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
        
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }
        
        c.Next()
    }
}

三、使用cors库 #

3.1 安装 #

bash
go get github.com/gin-contrib/cors

3.2 基本配置 #

go
import "github.com/gin-contrib/cors"

func main() {
    r := gin.Default()
    
    r.Use(cors.New(cors.Config{
        AllowOrigins:     []string{"http://localhost:3000"},
        AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
        AllowHeaders:     []string{"Content-Type", "Authorization"},
        ExposeHeaders:    []string{"Content-Length"},
        AllowCredentials: true,
        MaxAge:           12 * time.Hour,
    }))
    
    r.Run()
}

3.3 允许所有源 #

go
r.Use(cors.New(cors.Config{
    AllowAllOrigins: true,
    AllowMethods:    []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowHeaders:    []string{"Content-Type", "Authorization"},
}))

3.4 动态源配置 #

go
r.Use(cors.New(cors.Config{
    AllowOriginFunc: func(origin string) bool {
        // 允许所有localhost端口
        if strings.HasPrefix(origin, "http://localhost") {
            return true
        }
        // 允许特定域名
        if strings.HasSuffix(origin, ".example.com") {
            return true
        }
        return false
    },
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE"},
    AllowHeaders:     []string{"Content-Type", "Authorization"},
    AllowCredentials: true,
}))

3.5 完整配置示例 #

go
func main() {
    r := gin.Default()
    
    config := cors.Config{
        // 允许的源
        AllowOrigins: []string{
            "http://localhost:3000",
            "http://localhost:8080",
            "https://example.com",
        },
        
        // 或使用函数动态判断
        AllowOriginFunc: func(origin string) bool {
            return strings.HasPrefix(origin, "http://localhost") ||
                   strings.HasSuffix(origin, ".example.com")
        },
        
        // 允许的方法
        AllowMethods: []string{
            "GET",
            "POST",
            "PUT",
            "DELETE",
            "OPTIONS",
            "PATCH",
        },
        
        // 允许的请求头
        AllowHeaders: []string{
            "Content-Type",
            "Authorization",
            "X-Requested-With",
            "X-Request-ID",
        },
        
        // 暴露的响应头
        ExposeHeaders: []string{
            "Content-Length",
            "X-Request-ID",
        },
        
        // 允许携带凭证
        AllowCredentials: true,
        
        // 预检请求缓存时间
        MaxAge: 12 * time.Hour,
    }
    
    r.Use(cors.New(config))
    
    r.Run()
}

四、预检请求处理 #

4.1 什么是预检请求 #

对于非简单请求,浏览器会先发送OPTIONS请求:

text
简单请求:
- 方法:GET、POST、HEAD
- 请求头:Accept、Content-Type(仅限简单值)

非简单请求:
- 方法:PUT、DELETE
- 请求头:Authorization、Content-Type: application/json

4.2 预检请求流程 #

text
1. 浏览器发送 OPTIONS /api/users
2. 服务器返回允许的方法和头
3. 浏览器发送实际请求 POST /api/users
4. 服务器返回数据

4.3 处理预检请求 #

go
func CORSMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        origin := c.GetHeader("Origin")
        
        // 设置CORS头
        c.Header("Access-Control-Allow-Origin", origin)
        c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
        c.Header("Access-Control-Allow-Credentials", "true")
        c.Header("Access-Control-Max-Age", "86400")
        
        // 处理预检请求
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }
        
        c.Next()
    }
}

五、携带凭证 #

5.1 允许Cookie #

go
r.Use(cors.New(cors.Config{
    AllowOrigins:     []string{"http://localhost:3000"}, // 不能使用*
    AllowCredentials: true,
    AllowMethods:     []string{"GET", "POST"},
    AllowHeaders:     []string{"Content-Type"},
}))

5.2 前端配置 #

javascript
// fetch
fetch('http://localhost:8080/api/users', {
    credentials: 'include',
})

// axios
axios.get('http://localhost:8080/api/users', {
    withCredentials: true,
})

5.3 注意事项 #

text
当 AllowCredentials 为 true 时:
1. AllowOrigins 不能使用 "*"
2. 必须指定具体的源
3. 前端需要设置 credentials: 'include'

六、常见问题 #

6.1 跨域错误示例 #

text
Access to XMLHttpRequest at 'http://localhost:8080/api/users' 
from origin 'http://localhost:3000' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

6.2 解决方案 #

go
// 确保CORS中间件在路由之前
r.Use(cors.New(cors.Config{
    AllowOrigins: []string{"http://localhost:3000"},
    AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
    AllowHeaders: []string{"Content-Type", "Authorization"},
}))

// 确保处理OPTIONS请求
if c.Request.Method == "OPTIONS" {
    c.AbortWithStatus(204)
    return
}

6.3 自定义请求头 #

go
AllowHeaders: []string{
    "Content-Type",
    "Authorization",
    "X-Custom-Header",  // 添加自定义请求头
},

七、生产环境配置 #

7.1 环境区分 #

go
func getCORSConfig() cors.Config {
    if gin.Mode() == gin.ReleaseMode {
        return cors.Config{
            AllowOrigins: []string{
                "https://example.com",
                "https://api.example.com",
            },
            AllowMethods:     []string{"GET", "POST", "PUT", "DELETE"},
            AllowHeaders:     []string{"Content-Type", "Authorization"},
            AllowCredentials: true,
            MaxAge:           12 * time.Hour,
        }
    }
    
    return cors.Config{
        AllowAllOrigins:  true,
        AllowMethods:     []string{"GET", "POST", "PUT", "DELETE"},
        AllowHeaders:     []string{"Content-Type", "Authorization"},
        AllowCredentials: false,
    }
}

func main() {
    r := gin.Default()
    r.Use(cors.New(getCORSConfig()))
    r.Run()
}

7.2 配置文件 #

yaml
cors:
  allowed_origins:
    - https://example.com
    - https://api.example.com
  allowed_methods:
    - GET
    - POST
    - PUT
    - DELETE
  allowed_headers:
    - Content-Type
    - Authorization
  allow_credentials: true
  max_age: 43200

八、总结 #

8.1 核心要点 #

要点 说明
CORS头 设置正确的响应头
预检请求 处理OPTIONS请求
凭证 AllowCredentials配置
安全 生产环境限制源

8.2 最佳实践 #

实践 说明
使用cors库 使用成熟的CORS库
环境区分 开发和生产不同配置
限制源 生产环境限制允许的源
缓存预检 设置合理的MaxAge

8.3 下一步 #

现在你已经掌握了CORS跨域,接下来让我们学习 日志系统,了解Gin的日志管理!

最后更新:2026-03-28