Files
zt-closet-manage-backend/internal/middleware/jwtauth.go
T
2026-09-11 01:47:20 +08:00

138 lines
4.1 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 (
"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
}