Files
lunar-mini/server/internal/middleware/middleware.go
T
gouki deb32f8fab
Build and Publish Server / build (push) Successful in 4m30s
feat(server): 商业化支付与订单系统 + Harness 改进
- 新增商业化模块:Stripe/Binance Pay 支付、邮箱登录、订单管理
- 新增数据模型:ReportProduct, ReportEntitlement, ReportSnapshot, PaymentEvent, EmailLoginCode
- 新增 API:/api/commerce/checkout, /api/commerce/webhooks/*, /api/commerce/orders/*
- 新增管理后台:订单列表、统计图表、统一 AdminHeader 组件
- 新增中间件测试:RateLimit, Auth, CORS 共 7 个测试用例
- 新增 AGENTS.md 项目代理指令文件
- 新增 REMINDER.md 商业化功能开发规范
- 修复 JWT 解析:添加 WithValidMethods 限制 HS256
- 修复 GetPayStatus:增加用户权限校验
- 更新 .gitignore 和 README.md
2026-09-19 18:57:23 +00:00

180 lines
4.5 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"
"sync"
"time"
"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 的 JWT(Authorization 头或 HttpOnly Cookie)
// 浏览器导航(GET 页面请求)未登录时渲染登录页;API 请求未登录时返回 401 JSON
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)
}
authed := false
if tokenString != "" {
cfg := config.Load()
userService := service.NewUserService()
authed = userService.IsAdminToken(tokenString, cfg.JWT.Secret)
}
if !authed {
// 页面导航(GET 且非 /admin/api/):返回登录页
if c.Request.Method == http.MethodGet && !strings.HasPrefix(c.Request.URL.Path, "/admin/api/") {
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "管理员登录",
"page": "login",
"url": c.Request.URL.Path,
})
c.Abort()
return
}
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "需要管理员权限",
})
c.Abort()
return
}
c.Next()
}
}
var rateLimitStore = struct {
sync.Mutex
requests map[string][]time.Time
}{requests: make(map[string][]time.Time)}
// RateLimit 限流中间件
func RateLimit() gin.HandlerFunc {
return func(c *gin.Context) {
now := time.Now()
key := c.ClientIP() + "|" + c.FullPath()
rateLimitStore.Lock()
windowStart := now.Add(-time.Minute)
recent := rateLimitStore.requests[key][:0]
for _, requestTime := range rateLimitStore.requests[key] {
if requestTime.After(windowStart) {
recent = append(recent, requestTime)
}
}
if len(recent) >= 10 {
rateLimitStore.requests[key] = recent
rateLimitStore.Unlock()
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"code": 429, "msg": "请求过于频繁"})
return
}
rateLimitStore.requests[key] = append(recent, now)
rateLimitStore.Unlock()
c.Next()
}
}