From 7e7381b2091ab6cf7b6b79f23e17e44ce3d40c93 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Thu, 9 Jul 2026 15:39:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(pay):=20httpapi=20=E5=8A=A0=20CORS(web=20?= =?UTF-8?q?=E8=B7=A8=E5=9F=9F=E4=B8=8B=E5=8D=95)+=20=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E5=88=B0=20pangolin1=20=E5=87=86=E5=A4=87(#34/34A)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit withCORS 中间件:仅白名单 origin(默认 pangolin.yanmeiai.com + pages.dev)返回 CORS 头、 OPTIONS 直接 204;New(svc, corsOrigins) + main 读 PAY_CORS_ORIGINS。为官网下单页跨域调 pay 铺路。 Co-Authored-By: Claude Opus 4.8 --- pay/cmd/paywatch/main.go | 2 +- pay/internal/httpapi/cors.go | 40 ++++++++++++++++++++++++++++ pay/internal/httpapi/handler.go | 7 ++--- pay/internal/httpapi/handler_test.go | 2 +- 4 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 pay/internal/httpapi/cors.go diff --git a/pay/cmd/paywatch/main.go b/pay/cmd/paywatch/main.go index 845f0ec..cdab577 100644 --- a/pay/cmd/paywatch/main.go +++ b/pay/cmd/paywatch/main.go @@ -114,7 +114,7 @@ func main() { defer stop() go w.Loop(ctx, time.Duration(pollSec)*time.Second) - srv := &http.Server{Addr: addr, Handler: httpapi.New(svc), ReadHeaderTimeout: 10 * time.Second} + srv := &http.Server{Addr: addr, Handler: httpapi.New(svc, env("PAY_CORS_ORIGINS", "https://pangolin.yanmeiai.com,https://pangolin-site.pages.dev")), ReadHeaderTimeout: 10 * time.Second} go func() { log.Info("pangolin-pay listening", "addr", addr, "poll_seconds", pollSec) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { diff --git a/pay/internal/httpapi/cors.go b/pay/internal/httpapi/cors.go new file mode 100644 index 0000000..2da07ca --- /dev/null +++ b/pay/internal/httpapi/cors.go @@ -0,0 +1,40 @@ +package httpapi + +import ( + "net/http" + "strings" +) + +// withCORS lets the browser storefront (pangolin website) call the order API +// cross-origin. Auth is server-to-server / bearer-less here (no cookies), so +// Allow-Credentials is not needed. Only whitelisted origins get CORS headers; +// OPTIONS preflight is answered directly. +func withCORS(next http.Handler, allowed map[string]bool) 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, OPTIONS") + h.Set("Access-Control-Allow-Headers", "Content-Type") + h.Set("Access-Control-Max-Age", "600") + } + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + next.ServeHTTP(w, r) + }) +} + +// parseOrigins builds the allowed-origin set from a comma-separated list. +func parseOrigins(csv string) map[string]bool { + m := map[string]bool{} + for _, o := range strings.Split(csv, ",") { + if o = strings.TrimSpace(o); o != "" { + m[o] = true + } + } + return m +} diff --git a/pay/internal/httpapi/handler.go b/pay/internal/httpapi/handler.go index eb18947..d8c2809 100644 --- a/pay/internal/httpapi/handler.go +++ b/pay/internal/httpapi/handler.go @@ -14,8 +14,9 @@ import ( type Handler struct{ svc *pay.Service } -// New wires the routes (Go 1.22 method+wildcard patterns). -func New(svc *pay.Service) http.Handler { +// New wires the routes (Go 1.22 method+wildcard patterns) and applies CORS for +// the whitelisted browser origins (comma-separated; empty = no CORS headers). +func New(svc *pay.Service, corsOrigins string) http.Handler { h := &Handler{svc: svc} mux := http.NewServeMux() mux.HandleFunc("POST /order", h.createOrder) @@ -24,7 +25,7 @@ func New(svc *pay.Service) http.Handler { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) - return mux + return withCORS(mux, parseOrigins(corsOrigins)) } type createReq struct { diff --git a/pay/internal/httpapi/handler_test.go b/pay/internal/httpapi/handler_test.go index c593ea7..7b22a57 100644 --- a/pay/internal/httpapi/handler_test.go +++ b/pay/internal/httpapi/handler_test.go @@ -16,7 +16,7 @@ const recvAddr = "TRecv00000000000000000000000000000A" func TestCreateGetAndConflict(t *testing.T) { st, _ := store.Open(":memory:") t.Cleanup(func() { _ = st.Close() }) - srv := httptest.NewServer(New(pay.New(st, pay.Config{ReceiveAddress: recvAddr}))) + srv := httptest.NewServer(New(pay.New(st, pay.Config{ReceiveAddress: recvAddr}), "https://pangolin.yanmeiai.com")) t.Cleanup(srv.Close) body, _ := json.Marshal(map[string]any{"user_ref": "u1", "sku": "pro-year", "amount": 5_000000})