From 98256f1f544aefa2ef38578fd8b8ba5f02ba15b2 Mon Sep 17 00:00:00 2001 From: MengStack Dev Date: Sat, 3 Oct 2026 04:02:57 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Swagger=20UI=20?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E5=8A=A0=E8=BD=BD=E5=A4=B1=E8=B4=A5=E5=B9=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=93=81=E7=89=8C=E6=A0=87=E8=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除全局 CSP 头(API 返回 JSON,CSP 意义不大) - 新增 Swagger 专用处理器,设置宽松的 CSP 允许 inline style 和 data URI - 拦截 swagger-ui.css 注入品牌 CSS,隐藏 SmartBear logo - 替换为 "MengStack API 文档 v0.3.0" 品牌标识 --- internal/app/app.go | 5 +- internal/app/middleware/security.go | 1 - internal/app/middleware/swagger.go | 132 ++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 internal/app/middleware/swagger.go diff --git a/internal/app/app.go b/internal/app/app.go index da9e5b6..d210971 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -34,9 +34,6 @@ import ( "go.uber.org/fx" "go.uber.org/zap" "gorm.io/gorm" - - swaggerFiles "github.com/swaggo/files" - ginSwagger "github.com/swaggo/gin-swagger" ) var Module = fx.Module("app", @@ -97,7 +94,7 @@ func newEngine( upgradeHandler := upgrade.NewHandler() r.GET("/api/upgrade/check", upgradeHandler.Check) - r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + r.GET("/swagger/*any", middleware.SwaggerHandler()) tenantResolver := tenant.NewResolver(cfg.Tenant) diff --git a/internal/app/middleware/security.go b/internal/app/middleware/security.go index 018ca50..83e5227 100644 --- a/internal/app/middleware/security.go +++ b/internal/app/middleware/security.go @@ -9,7 +9,6 @@ func SecurityHeaders() gin.HandlerFunc { c.Header("X-Frame-Options", "DENY") c.Header("X-XSS-Protection", "1; mode=block") c.Header("Referrer-Policy", "strict-origin-when-cross-origin") - c.Header("Content-Security-Policy", "default-src 'self'") c.Next() } } diff --git a/internal/app/middleware/swagger.go b/internal/app/middleware/swagger.go new file mode 100644 index 0000000..97699ae --- /dev/null +++ b/internal/app/middleware/swagger.go @@ -0,0 +1,132 @@ +package middleware + +import ( + "bufio" + "bytes" + "net" + "net/http" + "strconv" + "strings" + + swaggerFiles "github.com/swaggo/files" + "github.com/gin-gonic/gin" +) + +// SwaggerHandler serves Swagger UI with relaxed CSP headers and MengStack +// branding CSS injected into swagger-ui.css. +func SwaggerHandler() gin.HandlerFunc { + return func(c *gin.Context) { + isCSS := strings.HasSuffix(c.Request.URL.Path, "swagger-ui.css") + + underlying := c.Writer + crw := &captureResponseWriter{ + ginWriter: underlying, + code: 200, + } + c.Writer = crw + + swaggerFiles.Handler.ServeHTTP(crw, c.Request) + + c.Writer = underlying + + c.Header("X-Content-Type-Options", "nosniff") + c.Header("X-Frame-Options", "DENY") + c.Header("X-XSS-Protection", "1; mode=block") + c.Header("Referrer-Policy", "strict-origin-when-cross-origin") + c.Header("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:") + + if isCSS && crw.code == 200 && crw.buf.Len() > 0 { + body := append(crw.buf.Bytes(), []byte(brandingCSS)...) + c.Header("Content-Length", strconv.Itoa(len(body))) + c.Header("Content-Type", "text/css; charset=utf-8") + underlying.Write(body) + return + } + + if crw.buf.Len() > 0 { + underlying.WriteHeader(crw.code) + underlying.Write(crw.buf.Bytes()) + return + } + + underlying.WriteHeader(crw.code) + } +} + +type captureResponseWriter struct { + ginWriter gin.ResponseWriter + code int + buf bytes.Buffer +} + +func (w *captureResponseWriter) Header() http.Header { return w.ginWriter.Header() } +func (w *captureResponseWriter) Write(b []byte) (int, error) { return w.buf.Write(b) } +func (w *captureResponseWriter) WriteHeader(code int) { w.code = code } +func (w *captureResponseWriter) WriteString(s string) (int, error) { return w.buf.WriteString(s) } +func (w *captureResponseWriter) Status() int { return w.code } +func (w *captureResponseWriter) Size() int { return w.buf.Len() } +func (w *captureResponseWriter) Written() bool { return w.buf.Len() > 0 } +func (w *captureResponseWriter) WriteHeaderNow() {} +func (w *captureResponseWriter) Pusher() http.Pusher { return nil } +func (w *captureResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return w.ginWriter.Hijack() +} +func (w *captureResponseWriter) CloseNotify() <-chan bool { return w.ginWriter.CloseNotify() } +func (w *captureResponseWriter) Flush() {} + +const brandingCSS = ` +/* MengStack API branding — replace Swagger SmartBear logo */ +.topbar { + display: flex !important; + align-items: center !important; + background-color: #1b1b1f !important; + padding: 8px 20px !important; + min-height: 50px !important; + position: relative !important; +} +.topbar > * { + visibility: hidden !important; + width: 0 !important; + height: 0 !important; + overflow: hidden !important; + margin: 0 !important; + padding: 0 !important; + flex: 0 !important; +} +.topbar::before { + content: "MengStack API \6587\6863" !important; + visibility: visible !important; + width: auto !important; + height: auto !important; + overflow: visible !important; + flex: 0 0 auto !important; + color: #fff !important; + font-size: 20px !important; + font-weight: 700 !important; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; + letter-spacing: 1px !important; + text-decoration: none !important; + position: absolute !important; + left: 20px !important; + top: 50% !important; + transform: translateY(-50%) !important; +} +.topbar::after { + content: "v0.3.0" !important; + visibility: visible !important; + width: auto !important; + height: auto !important; + overflow: visible !important; + flex: 0 0 auto !important; + color: rgba(255,255,255,0.5) !important; + font-size: 12px !important; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; + position: absolute !important; + left: 230px !important; + top: 50% !important; + transform: translateY(-50%) !important; +} +.swagger-ui { + margin-top: 0 !important; +} +`