138 lines
4.1 KiB
Go
138 lines
4.1 KiB
Go
package middleware
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"log/slog"
|
||
"strings"
|
||
|
||
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqldao"
|
||
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/redisdao"
|
||
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/pkg/app"
|
||
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/pkg/errcode"
|
||
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/pkg/lzcwyjwt"
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/redis/go-redis/v9"
|
||
)
|
||
|
||
type ctxKey string
|
||
|
||
const (
|
||
adminIDKey ctxKey = "adminID"
|
||
)
|
||
|
||
const (
|
||
// adminRoleSuper 超级管理员角色(账号管理接口专用)。
|
||
adminRoleSuper int8 = 1
|
||
// adminStatusEnabled 启用状态。
|
||
adminStatusEnabled int8 = 1
|
||
)
|
||
|
||
// JWTAuth 管理员鉴权中间件:
|
||
// 解析 Bearer token → Redis 会话校验(失败 fail-open)→ 数据库状态回查(禁用即拒绝)→ AdminID 注入 context。
|
||
func JWTAuth() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
response := app.NewResponse(c)
|
||
|
||
// 1. 从 Authorization 头取 token:Bearer xxx.yyy.zzz
|
||
authHeader := c.GetHeader("Authorization")
|
||
if authHeader == "" {
|
||
response.ToErrorResponse(errcode.UnauthorizedTokenMissing)
|
||
c.Abort()
|
||
return
|
||
}
|
||
|
||
parts := strings.SplitN(authHeader, " ", 2)
|
||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||
response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("invalid Authorization format"))
|
||
c.Abort()
|
||
return
|
||
}
|
||
|
||
// 2. 解析 token
|
||
claims, err := lzcwyjwt.ParseAccessToken(parts[1])
|
||
if err != nil {
|
||
switch {
|
||
case errors.Is(err, lzcwyjwt.ErrTokenExpired):
|
||
response.ToErrorResponse(errcode.UnauthorizedTokenTimeout)
|
||
case errors.Is(err, lzcwyjwt.ErrTokenWrongType):
|
||
response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("token type mismatch"))
|
||
default:
|
||
response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails(err.Error()))
|
||
}
|
||
c.Abort()
|
||
return
|
||
}
|
||
|
||
// 2.5 校验 access token 是否仍在会话中(登出/失效后立即拒绝)。
|
||
sessionID, serr := redisdao.AuthAdminIDByAccess(c.Request.Context(), parts[1])
|
||
if serr != nil {
|
||
if errors.Is(serr, redis.Nil) {
|
||
response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("token session invalid"))
|
||
c.Abort()
|
||
return
|
||
}
|
||
// Redis 其它错误降级放行(fail-open):优先保证可用性;
|
||
// 被禁用/删除的管理员仍会被下方 DB 回查拒绝,不会因本降级放行而绕过。
|
||
slog.WarnContext(c.Request.Context(), "check admin token session failed, allow by fallback", "error", serr)
|
||
} else if sessionID != claims.AdminID {
|
||
response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("token user mismatch"))
|
||
c.Abort()
|
||
return
|
||
}
|
||
|
||
// 3. 数据库回查管理员状态(禁用/删除立即生效)。
|
||
admin, dbErr := mysqldao.NewAdminDao().AdminGetByID(c.Request.Context(), claims.AdminID)
|
||
if dbErr != nil {
|
||
response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("管理员不存在"))
|
||
c.Abort()
|
||
return
|
||
}
|
||
if admin.Status != adminStatusEnabled {
|
||
response.ToErrorResponse(errcode.AdminDisabled)
|
||
c.Abort()
|
||
return
|
||
}
|
||
|
||
// 4. 把 adminID 注入 context
|
||
ctx := context.WithValue(c.Request.Context(), adminIDKey, claims.AdminID)
|
||
c.Request = c.Request.WithContext(ctx)
|
||
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// AdminSuperAuth 超级管理员权限校验(在 JWTAuth 之后使用)。
|
||
func AdminSuperAuth() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
response := app.NewResponse(c)
|
||
|
||
adminID, ok := GetAdminID(c.Request.Context())
|
||
if !ok {
|
||
response.ToErrorResponse(errcode.Unauthorized)
|
||
c.Abort()
|
||
return
|
||
}
|
||
|
||
admin, err := mysqldao.NewAdminDao().AdminGetByID(c.Request.Context(), adminID)
|
||
if err != nil {
|
||
response.ToErrorResponse(errcode.AdminNotFound)
|
||
c.Abort()
|
||
return
|
||
}
|
||
if admin.Role != adminRoleSuper {
|
||
response.ToErrorResponse(errcode.Forbidden.WithDetails("仅超级管理员可操作"))
|
||
c.Abort()
|
||
return
|
||
}
|
||
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// GetAdminID 从 context 取登录管理员 ID(在 handler 里用)。
|
||
func GetAdminID(ctx context.Context) (int64, bool) {
|
||
uid, ok := ctx.Value(adminIDKey).(int64)
|
||
return uid, ok
|
||
}
|