Files
lunar-mini/server/internal/middleware/middleware.go
T
gouki d175777d75
Build and Publish Server / build (push) Successful in 2m11s
fix(admin): 浏览器直接访问后台时渲染登录页而非401 JSON
- AdminAuth 中间件:GET 页面请求未登录时返回 login 页,API 请求仍返回 401
- 新增 Login.js 登录页组件(密码输入 + 登录后跳回原页面)
- index.html 注册 login 页面路由
2026-08-11 21:23:09 +00:00

156 lines
3.8 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
// 浏览器导航(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()
}
}
// RateLimit 限流中间件
func RateLimit() gin.HandlerFunc {
return func(c *gin.Context) {
// TODO: 实现限流
c.Next()
}
}