update
This commit is contained in:
57
internal/middleware/auth.go
Normal file
57
internal/middleware/auth.go
Normal 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
|
||||
}
|
||||
55
internal/middleware/cors.go
Normal file
55
internal/middleware/cors.go
Normal 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()
|
||||
}
|
||||
}
|
||||
40
internal/middleware/ssg_token.go
Normal file
40
internal/middleware/ssg_token.go
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user