Files
lunar-mini/server/internal/middleware/middleware.go
T
gouki b8c439deac fix(server): 安全加固与版本链路修复
- 微信登录改为真实 jscode2session,session_key 不再下发客户端
- 支付回调增加 HMAC 验签(X-Pay-Sign)与幂等处理,未配置密钥时拒绝回调
- 订单金额一律以服务端商品表定价,禁止客户端传入金额
- 付费许愿改为支付成功后创建,不再先许愿后付款
- 管理后台增加登录认证(ADMIN_PASSWORD + role=admin JWT + HttpOnly Cookie)
- 订单详情/取消增加本人归属校验,修复越权访问
- 版本信息改为 ldflags 注入单一链路,GoVersion 用 runtime.Version()
- 恢复 gin 默认访问日志(原 Logger 中间件输出为空)
- 加载 HTML 模板修复后台页面 500;godotenv 加载 .env.local
- CORS 支持 CORS_ORIGINS 白名单配置
2026-08-09 00:09:24 +00:00

149 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/gouki/lunar-server/internal/config"
"github.com/gouki/lunar-server/internal/service"
)
// CORS 跨域中间件;可通过 CORS_ORIGINS 配置允许的源(逗号分隔),未配置时保持 *
func CORS() gin.HandlerFunc {
cfg := config.Load()
origins := strings.TrimSpace(cfg.Server.CORSOrigins)
allowAll := origins == ""
allowed := map[string]bool{}
if !allowAll {
for _, o := range strings.Split(origins, ",") {
o = strings.TrimSpace(o)
if o != "" {
allowed[o] = true
}
}
}
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
if origin != "" {
if allowAll {
c.Header("Access-Control-Allow-Origin", "*")
} else if allowed[origin] {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Vary", "Origin")
} else {
// 非白名单源:不输出 CORS 头,浏览器会拦截跨域请求
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
return
}
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Pay-Sign")
}
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
// bearerToken 从 Authorization 头提取 Bearer token,格式错误时返回 ok=false 并已应答
func bearerToken(c *gin.Context) (string, bool) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "未授权",
})
c.Abort()
return "", false
}
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "token格式错误",
})
c.Abort()
return "", false
}
return parts[1], true
}
// Auth JWT认证中间件
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString, ok := bearerToken(c)
if !ok {
return
}
cfg := config.Load()
userService := service.NewUserService()
userID, err := userService.ParseToken(tokenString, cfg.JWT.Secret)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "token无效",
})
c.Abort()
return
}
c.Set("userID", userID)
c.Next()
}
}
// AdminAuth 管理员认证中间件:要求携带 role=admin 的 JWTAuthorization 头或 HttpOnly Cookie
func AdminAuth() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := ""
authHeader := c.GetHeader("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
tokenString = strings.TrimPrefix(authHeader, "Bearer ")
} else {
// 浏览器导航场景:登录时写入的 HttpOnly Cookie
tokenString, _ = c.Cookie(service.AdminTokenCookie)
}
if tokenString == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "需要管理员权限",
})
c.Abort()
return
}
cfg := config.Load()
userService := service.NewUserService()
if !userService.IsAdminToken(tokenString, cfg.JWT.Secret) {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "需要管理员权限",
})
c.Abort()
return
}
c.Next()
}
}
// RateLimit 限流中间件
func RateLimit() gin.HandlerFunc {
return func(c *gin.Context) {
// TODO: 实现限流
c.Next()
}
}