Files

120 lines
2.9 KiB
Go
Raw Permalink Normal View History

2026-05-14 11:05:20 +08:00
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func SecurityHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
h := c.Writer.Header()
h.Set("X-Content-Type-Options", "nosniff")
h.Set("X-Frame-Options", "DENY")
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
h.Set("X-Permitted-Cross-Domain-Policies", "none")
h.Set("Cross-Origin-Opener-Policy", "same-origin")
h.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()")
if isHTTPSRequest(c) {
h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
if isSensitivePath(c.Request.URL.Path) {
h.Set("Cache-Control", "no-store")
h.Set("Pragma", "no-cache")
}
c.Next()
}
}
func isHTTPSRequest(c *gin.Context) bool {
if c == nil || c.Request == nil {
return false
}
if c.Request.TLS != nil {
return true
}
return strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https")
}
func isSensitivePath(path string) bool {
switch strings.TrimSpace(path) {
case "/api/auth/login",
"/api/auth/refresh",
"/api/auth/me",
"/api/auth/logout",
"/api/auth/password-key",
"/api/ops/auth/login",
"/api/ops/auth/me",
"/api/ops/auth/password",
"/api/ops/auth/password-key":
return true
default:
return false
}
}
type CORSConfig struct {
AllowedOrigins []string
AllowAll bool
}
func CORSWithConfig(cfg CORSConfig) gin.HandlerFunc {
allowedOrigins := compactOrigins(cfg.AllowedOrigins)
allowedSet := make(map[string]struct{}, len(allowedOrigins))
for _, origin := range allowedOrigins {
allowedSet[origin] = struct{}{}
}
return func(c *gin.Context) {
origin := strings.TrimSpace(c.GetHeader("Origin"))
allowedOrigin := ""
switch {
case cfg.AllowAll:
allowedOrigin = "*"
case origin != "":
if _, ok := allowedSet[origin]; ok {
allowedOrigin = origin
}
}
if allowedOrigin != "" {
c.Header("Access-Control-Allow-Origin", allowedOrigin)
if allowedOrigin != "*" {
c.Header("Vary", "Origin")
}
}
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization, X-Request-ID, X-Internal-Metrics-Token")
c.Header("Access-Control-Expose-Headers", "X-Request-ID")
c.Header("Access-Control-Max-Age", "86400")
if c.Request.Method == http.MethodOptions {
if origin != "" && allowedOrigin == "" {
c.AbortWithStatus(http.StatusForbidden)
return
}
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
func compactOrigins(values []string) []string {
out := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
out = append(out, value)
}
return out
}