Files
jiang13-bbs/backend/middleware/auth.go
freefire a7b6421840 feat: 实现完整的板块级RBAC内容审核系统
- 新增站长/超级管理员/管理员/板块管理员四级角色体系
- 实现实时权限快照加载与细粒度权限校验
- 新增内容审核队列与前后端页面
- 重构用户权限管理与站点管理逻辑
- 新增评论/帖子审核状态与通知推送
- 优化前端权限控制与角色徽章展示
- 修正数据库迁移与默认值问题
2026-09-15 04:47:34 +08:00

175 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"
"strings"
"github.com/freefire/jiang13-bbs/model"
"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()
}
}
// RequireAdmin 必须管理员(保留兼容;等价于"管理员及以上",不含纯板块管理员)
func (m *AuthMiddleware) RequireAdmin() 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
}
if model.RoleLevel(model.Role(user.Role)) < model.RoleLevel(model.RoleAdmin) {
c.AbortWithStatusJSON(http.StatusForbidden, 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()
}
}
// RequirePerm 功能点鉴权,必须接在 RequireStaff 之后
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 读取,回退 Authorization header
// 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 == "" {
auth := c.GetHeader("Authorization")
if auth == "" {
return nil, false
}
tokenStr = strings.TrimPrefix(auth, "Bearer ")
if tokenStr == auth {
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
}