Files
jiang13-bbs/backend/middleware/auth.go
freefire ff2ab286fb feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等
新增:
- 书库导入导出(library_import/library_export)及测试
- 图片变体生成(image_variants)与响应式图片(responsiveImage)
- 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件
- 小组件运行时(widgetRuntime)与静态检查(widgetLint)
- 上传缓存中间件(upload_cache)与字体 CSS 提取脚本

其它:
- 后端 handlers/services 全量调整
- 前端页面、组件、库函数与配置更新
2026-10-01 03:06:05 +08:00

170 lines
4.7 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 (
"errors"
"net/http"
"github.com/freefire/jiang13-bbs/service"
"github.com/gin-gonic/gin"
)
// AccountBannedKey 写入 gin.Context 的标记:当前凭据所属账号已被封禁。
// parseToken 失败原因对 handler 不可见,通过 context 显式传递,
// 以便 /me 等 OptionalAuth 接口也能告知前端"被封禁"而非"未登录"。
const AccountBannedKey = "account_banned"
// 封禁响应体:code 供前端机器识别,error 供直接展示
func bannedJSON(c *gin.Context) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "账号已被封禁",
"code": "account_banned",
})
}
// AuthMiddleware 认证中间件
type AuthMiddleware struct {
auth *service.AuthService
}
func NewAuthMiddleware(auth *service.AuthService) *AuthMiddleware {
return &AuthMiddleware{auth: auth}
}
// OptionalAuth 可选登录:解析 token,失败不阻断
func (m *AuthMiddleware) OptionalAuth() gin.HandlerFunc {
return func(c *gin.Context) {
user, ok := m.parseToken(c)
if ok {
c.Set("user", user)
}
c.Next()
}
}
// RequireAuth 必须登录
func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
return func(c *gin.Context) {
user, ok := m.parseToken(c)
if !ok {
if c.GetBool(AccountBannedKey) {
bannedJSON(c)
return
}
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
return
}
c.Set("user", user)
c.Next()
}
}
// ActorKey 实时权限快照在 gin.Context 中的键
const ActorKey = "actor"
// RequireStaff 必须是管理团队成员(板块管理员及以上),
// 并把实时 Actor(角色+板块授权)写入 context 供 RequirePerm/handler 使用
func (m *AuthMiddleware) RequireStaff() gin.HandlerFunc {
return func(c *gin.Context) {
claims, ok := m.parseToken(c)
if !ok {
if c.GetBool(AccountBannedKey) {
bannedJSON(c)
return
}
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
return
}
actor, err := m.auth.LoadActor(claims.ID)
if err != nil || !actor.IsStaff() {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
return
}
c.Set("user", claims)
c.Set(ActorKey, actor)
c.Next()
}
}
// RequireActor 必须登录并加载实时 Actor(不要求管理角色),
// 供账号级渠道权限(非管理角色也可被单独授予)所在路由组使用;
// 功能点鉴权由后续 RequirePerm 完成
func (m *AuthMiddleware) RequireActor() gin.HandlerFunc {
return func(c *gin.Context) {
claims, ok := m.parseToken(c)
if !ok {
if c.GetBool(AccountBannedKey) {
bannedJSON(c)
return
}
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
return
}
actor, err := m.auth.LoadActor(claims.ID)
if err != nil {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "无权限执行该操作"})
return
}
c.Set("user", claims)
c.Set(ActorKey, actor)
c.Next()
}
}
// RequirePerm 功能点鉴权,必须接在 RequireStaff / RequireActor 之后
func (m *AuthMiddleware) RequirePerm(perm string) gin.HandlerFunc {
return func(c *gin.Context) {
actor := CurrentActor(c)
if !actor.HasPerm(perm) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "无权限执行该操作"})
return
}
c.Next()
}
}
// parseToken 解析并校验 token:
// 1. 从 HttpOnly cookie(j13_token / 生产 __Host-j13_token)读取
// 2. 校验 JWT 签名和过期
// 3. 查 DB 实时校验 token_version 和 banned 状态
func (m *AuthMiddleware) parseToken(c *gin.Context) (*service.UserClaims, bool) {
tokenStr, err := c.Cookie(service.CookieName)
if err != nil || tokenStr == "" {
return nil, false
}
claims, err := m.auth.ParseToken(tokenStr)
if err != nil {
return nil, false
}
// 实时校验:token_version 匹配 + 未封禁(防止旧 JWT 在封禁/改密码后仍有效)
if _, err := m.auth.ValidateClaims(claims); err != nil {
// 封禁原因写入 context:401 与 403 的区分由上层中间件/handler 完成
if errors.Is(err, service.ErrAccountBanned) {
c.Set(AccountBannedKey, true)
}
return nil, false
}
// 异步刷新在线心跳(SQL 每 60s 限频一次),不阻塞请求
go m.auth.TouchLastSeen(claims.ID)
return claims, true
}
// CurrentUser 从 context 获取当前用户
func CurrentUser(c *gin.Context) *service.UserClaims {
if v, ok := c.Get("user"); ok {
if u, ok := v.(*service.UserClaims); ok {
return u
}
}
return nil
}
// CurrentActor 从 context 获取当前操作者的实时权限快照(RequireStaff 写入)
func CurrentActor(c *gin.Context) *service.Actor {
if v, ok := c.Get(ActorKey); ok {
if a, ok := v.(*service.Actor); ok {
return a
}
}
return nil
}