mirror of
https://git.fightbot.fun/hxuanyu/BingPaper.git
synced 2026-02-15 10:19:32 +08:00
255 lines
6.5 KiB
Markdown
255 lines
6.5 KiB
Markdown
# Go 后端 CORS 配置示例
|
||
|
||
## 问题说明
|
||
|
||
开发环境中,前端运行在 `http://localhost:5173`,后端运行在 `http://localhost:8080`。
|
||
由于跨域限制,需要在后端配置 CORS 才能正常访问 API。
|
||
|
||
## 使用 Gin 框架
|
||
|
||
### 1. 安装 CORS 中间件
|
||
|
||
```bash
|
||
go get github.com/gin-contrib/cors
|
||
```
|
||
|
||
### 2. 配置 CORS
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/gin-contrib/cors"
|
||
"time"
|
||
)
|
||
|
||
func main() {
|
||
r := gin.Default()
|
||
|
||
// 开发环境:配置 CORS
|
||
if gin.Mode() == gin.DebugMode {
|
||
config := cors.Config{
|
||
AllowOrigins: []string{"http://localhost:5173"},
|
||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
|
||
ExposeHeaders: []string{"Content-Length"},
|
||
AllowCredentials: true,
|
||
MaxAge: 12 * time.Hour,
|
||
}
|
||
r.Use(cors.New(config))
|
||
}
|
||
|
||
// API 路由
|
||
api := r.Group("/api/v1")
|
||
{
|
||
// 图片相关
|
||
api.GET("/images", getImages)
|
||
api.GET("/image/today/meta", getTodayImageMeta)
|
||
api.GET("/image/date/:date/meta", getImageMetaByDate)
|
||
api.GET("/image/random/meta", getRandomImageMeta)
|
||
|
||
// 管理员相关
|
||
api.POST("/admin/login", adminLogin)
|
||
// ... 其他路由
|
||
}
|
||
|
||
// 生产环境:静态文件服务
|
||
if gin.Mode() == gin.ReleaseMode {
|
||
r.Static("/assets", "./web/assets")
|
||
r.StaticFile("/", "./web/index.html")
|
||
|
||
// SPA fallback
|
||
r.NoRoute(func(c *gin.Context) {
|
||
c.File("./web/index.html")
|
||
})
|
||
}
|
||
|
||
r.Run(":8080")
|
||
}
|
||
```
|
||
|
||
### 3. 更灵活的 CORS 配置
|
||
|
||
```go
|
||
// 根据环境变量动态配置
|
||
func setupCORS(r *gin.Engine) {
|
||
allowOrigins := os.Getenv("ALLOW_ORIGINS")
|
||
if allowOrigins == "" {
|
||
allowOrigins = "http://localhost:5173"
|
||
}
|
||
|
||
origins := strings.Split(allowOrigins, ",")
|
||
|
||
config := cors.Config{
|
||
AllowOrigins: origins,
|
||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
|
||
ExposeHeaders: []string{"Content-Length"},
|
||
AllowCredentials: true,
|
||
MaxAge: 12 * time.Hour,
|
||
}
|
||
|
||
r.Use(cors.New(config))
|
||
}
|
||
|
||
func main() {
|
||
r := gin.Default()
|
||
|
||
// 只在开发环境启用 CORS
|
||
if gin.Mode() == gin.DebugMode {
|
||
setupCORS(r)
|
||
}
|
||
|
||
// ... 其他配置
|
||
}
|
||
```
|
||
|
||
## 使用标准库
|
||
|
||
如果不使用 Gin 框架,可以手动实现 CORS:
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"net/http"
|
||
)
|
||
|
||
func corsMiddleware(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
// 设置 CORS 头
|
||
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173")
|
||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||
w.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization")
|
||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||
w.Header().Set("Access-Control-Max-Age", "43200") // 12 hours
|
||
|
||
// 处理预检请求
|
||
if r.Method == "OPTIONS" {
|
||
w.WriteHeader(http.StatusOK)
|
||
return
|
||
}
|
||
|
||
next.ServeHTTP(w, r)
|
||
})
|
||
}
|
||
|
||
func main() {
|
||
mux := http.NewServeMux()
|
||
|
||
// 注册路由
|
||
mux.HandleFunc("/api/v1/images", getImages)
|
||
// ... 其他路由
|
||
|
||
// 包装 CORS 中间件
|
||
handler := corsMiddleware(mux)
|
||
|
||
http.ListenAndServe(":8080", handler)
|
||
}
|
||
```
|
||
|
||
## 配置文件方式
|
||
|
||
可以通过配置文件管理 CORS 设置:
|
||
|
||
```yaml
|
||
# config.yaml
|
||
server:
|
||
port: 8080
|
||
mode: debug # debug 或 release
|
||
|
||
cors:
|
||
enabled: true
|
||
allow_origins:
|
||
- http://localhost:5173
|
||
- http://127.0.0.1:5173
|
||
allow_methods:
|
||
- GET
|
||
- POST
|
||
- PUT
|
||
- PATCH
|
||
- DELETE
|
||
- OPTIONS
|
||
allow_headers:
|
||
- Origin
|
||
- Content-Type
|
||
- Accept
|
||
- Authorization
|
||
allow_credentials: true
|
||
max_age: 43200
|
||
```
|
||
|
||
```go
|
||
type CORSConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
AllowOrigins []string `yaml:"allow_origins"`
|
||
AllowMethods []string `yaml:"allow_methods"`
|
||
AllowHeaders []string `yaml:"allow_headers"`
|
||
AllowCredentials bool `yaml:"allow_credentials"`
|
||
MaxAge int `yaml:"max_age"`
|
||
}
|
||
|
||
func setupCORSFromConfig(r *gin.Engine, cfg *CORSConfig) {
|
||
if !cfg.Enabled {
|
||
return
|
||
}
|
||
|
||
config := cors.Config{
|
||
AllowOrigins: cfg.AllowOrigins,
|
||
AllowMethods: cfg.AllowMethods,
|
||
AllowHeaders: cfg.AllowHeaders,
|
||
AllowCredentials: cfg.AllowCredentials,
|
||
MaxAge: time.Duration(cfg.MaxAge) * time.Second,
|
||
}
|
||
|
||
r.Use(cors.New(config))
|
||
}
|
||
```
|
||
|
||
## 安全建议
|
||
|
||
1. **生产环境不要启用 CORS**:生产环境前后端在同一域名下,不需要 CORS
|
||
2. **限制允许的源**:不要使用 `*`,明确指定允许的域名
|
||
3. **使用环境变量**:允许的源应该通过环境变量配置,不要硬编码
|
||
4. **限制方法和头**:只允许必要的 HTTP 方法和请求头
|
||
5. **注意凭证**:如果使用 `AllowCredentials: true`,不能使用通配符源
|
||
|
||
## 测试 CORS 配置
|
||
|
||
可以使用 curl 测试 CORS:
|
||
|
||
```bash
|
||
# 测试预检请求
|
||
curl -X OPTIONS http://localhost:8080/api/v1/images \
|
||
-H "Origin: http://localhost:5173" \
|
||
-H "Access-Control-Request-Method: GET" \
|
||
-v
|
||
|
||
# 测试实际请求
|
||
curl -X GET http://localhost:8080/api/v1/images \
|
||
-H "Origin: http://localhost:5173" \
|
||
-v
|
||
```
|
||
|
||
应该能看到响应头中包含:
|
||
```
|
||
Access-Control-Allow-Origin: http://localhost:5173
|
||
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
|
||
Access-Control-Allow-Headers: Origin, Content-Type, Accept, Authorization
|
||
Access-Control-Allow-Credentials: true
|
||
```
|
||
|
||
## 常见问题
|
||
|
||
### Q: 为什么要区分开发和生产环境?
|
||
A: 生产环境前后端部署在同一域名下,不需要 CORS。只在开发环境需要。
|
||
|
||
### Q: 可以使用代理吗?
|
||
A: 可以在 Vite 中配置代理,但不推荐。直接配置 CORS 更接近生产环境,便于发现问题。
|
||
|
||
### Q: 如何处理认证?
|
||
A: 如果使用 Cookie 或需要发送凭证,必须设置 `AllowCredentials: true` 和明确的源。
|
||
|
||
### Q: 预检请求是什么?
|
||
A: 浏览器在某些跨域请求前会发送 OPTIONS 请求,询问服务器是否允许该跨域请求。 |