fix: 修复 Swagger UI 图标加载失败并添加品牌标识
Some checks failed
CI / Build & Test (push) Has been cancelled

- 移除全局 CSP 头(API 返回 JSON,CSP 意义不大)
- 新增 Swagger 专用处理器,设置宽松的 CSP 允许 inline style 和 data URI
- 拦截 swagger-ui.css 注入品牌 CSS,隐藏 SmartBear logo
- 替换为 "MengStack API 文档 v0.3.0" 品牌标识
This commit is contained in:
MengStack Dev 2026-10-03 04:02:57 +08:00
parent d43205540d
commit 98256f1f54
3 changed files with 133 additions and 5 deletions

View File

@ -34,9 +34,6 @@ import (
"go.uber.org/fx" "go.uber.org/fx"
"go.uber.org/zap" "go.uber.org/zap"
"gorm.io/gorm" "gorm.io/gorm"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
) )
var Module = fx.Module("app", var Module = fx.Module("app",
@ -97,7 +94,7 @@ func newEngine(
upgradeHandler := upgrade.NewHandler() upgradeHandler := upgrade.NewHandler()
r.GET("/api/upgrade/check", upgradeHandler.Check) 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) tenantResolver := tenant.NewResolver(cfg.Tenant)

View File

@ -9,7 +9,6 @@ func SecurityHeaders() gin.HandlerFunc {
c.Header("X-Frame-Options", "DENY") c.Header("X-Frame-Options", "DENY")
c.Header("X-XSS-Protection", "1; mode=block") c.Header("X-XSS-Protection", "1; mode=block")
c.Header("Referrer-Policy", "strict-origin-when-cross-origin") c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
c.Header("Content-Security-Policy", "default-src 'self'")
c.Next() c.Next()
} }
} }

View File

@ -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;
}
`