usercenter(app.yanmeiai.com,浏览器)跨域调控制面 API 会被浏览器拦(无 CORS 头)。 加白名单 CORS 中间件(CORS_ORIGINS,缺省 app.yanmeiai.com + pages.dev),应答 OPTIONS 预检。原生端非浏览器不受影响。Bearer 认证故不需 Allow-Credentials。含 3 项测试。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
@@ -123,6 +123,8 @@ func main() {
|
||||
r.Use(chimw.Logger)
|
||||
r.Use(chimw.Recoverer)
|
||||
r.Use(apierr.Middleware)
|
||||
// CORS:Web 用户中心(app.yanmeiai.com)跨域调 /v1/*;原生端不受影响。
|
||||
r.Use(httpapi.NewCORS())
|
||||
|
||||
r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NewCORS 返回一个 CORS 中间件:为白名单 Origin(精确匹配)补 CORS 响应头,并直接
|
||||
// 应答 OPTIONS 预检。Web 用户中心(app.yanmeiai.com,浏览器)跨域调用 /v1/*,浏览器
|
||||
// 会先发预检、并校验 Access-Control-Allow-Origin;原生移动/桌面客户端不是浏览器、
|
||||
// 不受 CORS 约束,故此前无需 CORS。
|
||||
//
|
||||
// Origin 白名单来自 CORS_ORIGINS(逗号分隔),缺省含 usercenter 的正式域与 pages.dev。
|
||||
// 认证走 Authorization: Bearer(非 cookie),所以不需要 Allow-Credentials。
|
||||
func NewCORS() func(http.Handler) http.Handler {
|
||||
raw := os.Getenv("CORS_ORIGINS")
|
||||
if raw == "" {
|
||||
raw = "https://app.yanmeiai.com,https://pangolin-usercenter.pages.dev"
|
||||
}
|
||||
allowed := map[string]bool{}
|
||||
for _, o := range strings.Split(raw, ",") {
|
||||
o = strings.TrimSpace(o)
|
||||
if o != "" {
|
||||
allowed[o] = true
|
||||
}
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin != "" && allowed[origin] {
|
||||
h := w.Header()
|
||||
h.Set("Access-Control-Allow-Origin", origin)
|
||||
h.Add("Vary", "Origin")
|
||||
h.Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
h.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
h.Set("Access-Control-Max-Age", "600")
|
||||
}
|
||||
// 预检请求直接 204(不落到业务路由,避免 /v1/... 的 OPTIONS 404)。
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCORS(t *testing.T) {
|
||||
t.Setenv("CORS_ORIGINS", "https://app.yanmeiai.com")
|
||||
mw := NewCORS()
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })
|
||||
h := mw(next)
|
||||
|
||||
// 允许的 Origin:补 Allow-Origin,普通请求继续。
|
||||
r := httptest.NewRequest("POST", "/v1/auth/login", nil)
|
||||
r.Header.Set("Origin", "https://app.yanmeiai.com")
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://app.yanmeiai.com" {
|
||||
t.Fatalf("allowed origin: want header, got %q", got)
|
||||
}
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("non-preflight should reach next, got %d", w.Code)
|
||||
}
|
||||
|
||||
// OPTIONS 预检:204,不落到业务。
|
||||
r = httptest.NewRequest("OPTIONS", "/v1/auth/login", nil)
|
||||
r.Header.Set("Origin", "https://app.yanmeiai.com")
|
||||
w = httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("preflight: want 204, got %d", w.Code)
|
||||
}
|
||||
if w.Header().Get("Access-Control-Allow-Methods") == "" {
|
||||
t.Fatalf("preflight missing Allow-Methods")
|
||||
}
|
||||
|
||||
// 未白名单 Origin:不补 Allow-Origin。
|
||||
r = httptest.NewRequest("POST", "/v1/auth/login", nil)
|
||||
r.Header.Set("Origin", "https://evil.example.com")
|
||||
w = httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Fatalf("disallowed origin should get no header, got %q", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user