How to handle CORS cross-origin issues in Go APIs? What are the solutions?
考察点:CORS跨域处理。
答案:
CORS(Cross-Origin Resource Sharing)是一种允许Web页面从不同域名访问资源的机制。在Go API开发中,需要正确配置CORS头部来解决浏览器的跨域限制问题。
CORS基本概念:
- 简单请求:GET、POST(特定Content-Type)、HEAD请求
- 预检请求:复杂请求前的OPTIONS请求,用于验证是否允许跨域
- 凭证请求:包含Cookie或Authorization头的请求
标准库CORS实现:
package main
import (
"log"
"net/http"
)
func enableCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "86400")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
func apiHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message": "Hello CORS!"}`))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/data", apiHandler)
handler := enableCORS(mux)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}
高级CORS配置:
type CORSConfig struct {
AllowedOrigins []string
AllowedMethods []string
AllowedHeaders []string
ExposedHeaders []string
AllowCredentials bool
MaxAge int
AllowPrivateNetwork bool
}
func NewCORSMiddleware(config CORSConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if len(config.AllowedOrigins) > 0 {
allowed := false
for _, allowedOrigin := range config.AllowedOrigins {
if allowedOrigin == "*" || allowedOrigin == origin {
allowed = true
break
}
}
if !allowed {
http.Error(w, "CORS: origin not allowed", http.StatusForbidden)
return
}
}
if origin != "" {
if contains(config.AllowedOrigins, "*") {
w.Header().Set("Access-Control-Allow-Origin", "*")
} else {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
}
if len(config.AllowedMethods) > 0 {
w.Header().Set("Access-Control-Allow-Methods",
strings.Join(config.AllowedMethods, ", "))
}
if len(config.AllowedHeaders) > 0 {
w.Header().Set("Access-Control-Allow-Headers",
strings.Join(config.AllowedHeaders, ", "))
}
if len(config.ExposedHeaders) > 0 {
w.Header().Set("Access-Control-Expose-Headers",
strings.Join(config.ExposedHeaders, ", "))
}
if config.AllowCredentials {
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
if config.MaxAge > 0 {
w.Header().Set("Access-Control-Max-Age", strconv.Itoa(config.MaxAge))
}
if config.AllowPrivateNetwork {
w.Header().Set("Access-Control-Allow-Private-Network", "true")
}
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
func main() {
corsConfig := CORSConfig{
AllowedOrigins: []string{"http://localhost:3000", "https://example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization", "X-API-Key"},
ExposedHeaders: []string{"X-Total-Count", "X-Page-Count"},
AllowCredentials: true,
MaxAge: 86400,
}
corsMiddleware := NewCORSMiddleware(corsConfig)
mux := http.NewServeMux()
mux.HandleFunc("/api/", apiHandler)
handler := corsMiddleware(mux)
log.Fatal(http.ListenAndServe(":8080", handler))
}
Gin框架CORS实现:
package main
import (
"net/http"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
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{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
r.GET("/api/data", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Hello CORS with Gin!",
})
})
r.Run(":8080")
}
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
allowedOrigins := []string{
"http://localhost:3000",
"https://yourdomain.com",
}
for _, allowedOrigin := range allowedOrigins {
if origin == allowedOrigin {
c.Header("Access-Control-Allow-Origin", origin)
break
}
}
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
处理复杂CORS场景:
func getCORSConfig() CORSConfig {
if os.Getenv("ENV") == "production" {
return CORSConfig{
AllowedOrigins: []string{
"https://yourdomain.com",
"https://app.yourdomain.com",
},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 86400,
}
}
return CORSConfig{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"*"},
AllowedHeaders: []string{"*"},
AllowCredentials: false,
MaxAge: 86400,
}
}
func isSubdomain(origin, domain string) bool {
return strings.HasSuffix(origin, "."+domain) || origin == domain
}
func matchOrigin(origin string, allowedOrigins []string) bool {
for _, allowed := range allowedOrigins {
if allowed == "*" {
return true
}
if matched, _ := filepath.Match(allowed, origin); matched {
return true
}
}
return false
}
CORS安全最佳实践:
func secureCORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
allowedOrigins := []string{
"https://yourdomain.com",
"https://app.yourdomain.com",
}
allowed := false
for _, allowedOrigin := range allowedOrigins {
if origin == allowedOrigin {
allowed = true
c.Header("Access-Control-Allow-Origin", origin)
break
}
}
if !allowed && origin != "" {
c.JSON(http.StatusForbidden, gin.H{
"error": "CORS: origin not allowed",
})
c.Abort()
return
}
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Max-Age", "3600")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
不同解决方案对比:
- 通配符(*):简单但不安全,不支持凭证
- 白名单:安全但需要维护域名列表
- 反向代理:通过Nginx等代理服务器处理CORS
- JSONP:仅支持GET请求的传统跨域方案
最佳实践:
- 生产环境避免使用通配符,明确指定允许的域名
- 合理设置MaxAge减少预检请求频率
- 只暴露必要的HTTP头部
- 根据API需求决定是否允许凭证
- 记录和监控CORS相关的错误请求
- 在开发和生产环境使用不同的CORS配置