This commit is contained in:
toom1996
2026-08-26 10:45:21 +08:00
parent 3cf1368e68
commit 30c9f21da4
48 changed files with 3442 additions and 0 deletions

View File

@ -0,0 +1,57 @@
// Package middleware Gin 中间件集合。
package middleware
import (
"net/http"
"strings"
"fashionapi/internal/pkg/jwt"
"fashionapi/internal/pkg/response"
"github.com/gin-gonic/gin"
)
// gin.Context 中存放身份信息的键。
const (
ContextUserID = "userID"
ContextUsername = "username"
)
// bearerPrefix Authorization 头的令牌前缀。
const bearerPrefix = "Bearer "
// Auth JWT 鉴权中间件:解析 Authorization: Bearer <token>,把身份写入 Context。
//
// 令牌签发器由外部注入,避免像原实现那样在每个请求里重新加载一次配置。
func Auth(manager *jwt.Manager) gin.HandlerFunc {
return func(c *gin.Context) {
header := c.GetHeader("Authorization")
if !strings.HasPrefix(header, bearerPrefix) {
response.AbortError(c, http.StatusUnauthorized, "missing token")
return
}
claims, err := manager.Parse(strings.TrimPrefix(header, bearerPrefix))
if err != nil {
response.AbortError(c, http.StatusUnauthorized, "invalid token")
return
}
c.Set(ContextUserID, claims.UserID)
c.Set(ContextUsername, claims.Username)
c.Next()
}
}
// UserIDFrom 从 Context 取出当前用户 id。
func UserIDFrom(c *gin.Context) (uint32, bool) {
v, exists := c.Get(ContextUserID)
if !exists {
return 0, false
}
id, ok := v.(uint32)
if !ok || id == 0 {
return 0, false
}
return id, true
}

View File

@ -0,0 +1,55 @@
package middleware
import (
"net/http"
"strconv"
"strings"
"fashionapi/internal/config"
"github.com/gin-gonic/gin"
)
// CORS 跨域中间件,规则来自 yml 配置。
//
// allow_origins 为 ["*"] 时直接放行全部来源(与原项目行为一致);
// 配置为具体域名列表时,仅当请求 Origin 命中白名单才回写该 Origin,
// 这样生产环境可以收紧到指定域名而无需改代码。
func CORS(cfg config.CORSConfig) gin.HandlerFunc {
allowAll := len(cfg.AllowOrigins) == 0
allowed := make(map[string]struct{}, len(cfg.AllowOrigins))
for _, o := range cfg.AllowOrigins {
if o == "*" {
allowAll = true
}
allowed[o] = struct{}{}
}
methods := strings.Join(cfg.AllowMethods, ", ")
headers := strings.Join(cfg.AllowHeaders, ", ")
maxAge := strconv.Itoa(cfg.MaxAge)
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
switch {
case allowAll:
c.Header("Access-Control-Allow-Origin", "*")
case origin != "":
if _, ok := allowed[origin]; ok {
c.Header("Access-Control-Allow-Origin", origin)
// 指定来源时必须声明 Vary,避免 CDN / 代理把响应错误地跨来源复用
c.Header("Vary", "Origin")
}
}
c.Header("Access-Control-Allow-Methods", methods)
c.Header("Access-Control-Allow-Headers", headers)
c.Header("Access-Control-Max-Age", maxAge)
// 预检请求直接结束,不进入业务逻辑
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}

View File

@ -0,0 +1,40 @@
package middleware
import (
"net/http"
"fashionapi/internal/pkg/response"
"github.com/gin-gonic/gin"
)
// SSGToken 是 SSG 内部端口的「防御纵深」鉴权中间件。
//
// 设计取舍:
// - token 为空字符串时直接放行(noop)。因为 SSG 端口本身只绑在 127.0.0.1(回环),
// 外部网络根本连不进来,本地开发/单机构建无需令牌也能保证安全。
// - token 非空时,请求必须携带 ?token=<secret> 或 X-SSG-Token: <secret> 头,
// 否则返回 401。用于「即便回环被意外暴露(如误绑 0.0.0.0)」时的最后一道闸。
//
// 注意:该中间件只装在 SSG 内部引擎上,对外公开引擎(8090)从不装载,
// 因此不会给公网接口引入任何额外逻辑。
func SSGToken(token string) gin.HandlerFunc {
// 未配置令牌:回环绑定已足够,直接放行。
if token == "" {
return func(c *gin.Context) { c.Next() }
}
return func(c *gin.Context) {
// 优先从查询参数取(astro build 的 node fetch 拼 URL 最方便),
// 其次从自定义请求头取(便于 curl / CI 手动调用)。
got := c.Query("token")
if got == "" {
got = c.GetHeader("X-SSG-Token")
}
if got != token {
response.AbortError(c, http.StatusUnauthorized, "invalid or missing ssg token")
return
}
c.Next()
}
}