Files

804 lines
24 KiB
Go
Raw Permalink 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 admin
import (
"NetworkAuth/controllers"
"NetworkAuth/database"
"NetworkAuth/models"
"NetworkAuth/services"
"NetworkAuth/utils"
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
// ============================================================================
// 全局变量
// ============================================================================
// 创建BaseController实例
var authBaseController = controllers.NewBaseController()
// ============================================================================
// API处理器
// ============================================================================
// CSRFTokenHandler 获取CSRF令牌接口
func CSRFTokenHandler(c *gin.Context) {
// 尝试从Cookie获取
var token string
if cookie, err := c.Cookie(CSRFCookieName); err == nil && cookie != "" {
token = cookie
} else {
newToken, err := utils.GenerateCSRFToken()
if err != nil {
authBaseController.HandleInternalError(c, "生成CSRF令牌失败", err)
return
}
token = newToken
setCSRFToken(c, token)
}
authBaseController.HandleSuccess(c, "success", gin.H{
"csrf_token": token,
})
}
// LoginHandler 管理员登录接口
// - 接收JSON: {username, password, captcha, csrf_token}
// - 验证CSRF令牌
// - 验证验证码
// - 验证用户存在与密码正确性
// - 仅允许管理员登录
// - 成功后设置JWT Cookie
func LoginHandler(c *gin.Context) {
var body struct {
Username string `json:"username"`
Password string `json:"password"`
Captcha string `json:"captcha"`
CSRFToken string `json:"csrf_token"`
}
if !authBaseController.BindJSON(c, &body) {
return
}
// 1. 验证CSRF令牌 (Gin 方式)
if !validateCSRFToken(c, body.CSRFToken) {
authBaseController.HandleValidationError(c, "CSRF令牌验证失败")
return
}
if !authBaseController.ValidateRequired(c, map[string]interface{}{
"用户名": body.Username,
"密码": body.Password,
"验证码": body.Captcha,
}) {
return
}
// 验证验证码
if !VerifyCaptcha(c, body.Captcha) {
recordLoginLog(c, "", body.Username, 0, "验证码错误或已过期")
authBaseController.HandleValidationError(c, "验证码错误或已过期")
return
}
// 从数据库中查找对应的用户
db, err := database.GetDB()
if err != nil {
recordLoginLog(c, "", body.Username, 0, "数据库连接失败")
authBaseController.HandleInternalError(c, "数据库连接失败", err)
return
}
var user models.User
if err := db.Where("username = ?", body.Username).First(&user).Error; err != nil {
recordLoginLog(c, user.UUID, body.Username, 0, "用户不存在")
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
return
}
// 检查账号状态 (Status=1 表示启用,否则禁止登录)
if user.Status != 1 {
recordLoginLog(c, user.UUID, body.Username, 0, "账号已被禁用")
authBaseController.HandleValidationError(c, "该账号已被禁用,请联系超级管理员")
return
}
// 检查是否允许登录 (role=0 或 role=1 允许登录role=2 不允许)
if user.Role > 1 {
recordLoginLog(c, user.UUID, body.Username, 0, "权限不足")
authBaseController.HandleValidationError(c, "权限不足,禁止登录")
return
}
// 验证密码(使用盐值校验)
if !utils.VerifyPasswordWithSalt(body.Password, user.PasswordSalt, user.Password) {
recordLoginLog(c, user.UUID, body.Username, 0, "密码错误")
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
return
}
// 生成 access JWT
token, err := generateJWTTokenForAdmin(user.Username, user.Password, user.UUID, user.Role)
if err != nil {
recordLoginLog(c, user.UUID, body.Username, 0, "生成令牌失败")
authBaseController.HandleInternalError(c, "生成令牌失败", err)
return
}
// 签发 refreshToken新 family
settingsService := services.GetSettingsService()
refreshTokenSvc := services.GetRefreshTokenService()
refreshDays := settingsService.GetRefreshTokenExpireDays()
absoluteDays := settingsService.GetSessionAbsoluteExpireDays()
if absoluteDays < refreshDays {
absoluteDays = refreshDays
}
refreshExpiresAt := time.Now().Add(time.Duration(refreshDays) * 24 * time.Hour)
absoluteExpiresAt := time.Now().Add(time.Duration(absoluteDays) * 24 * time.Hour)
jti := refreshTokenSvc.NewJTI()
familyID := refreshTokenSvc.NewFamilyID()
refreshToken, err := generateRefreshTokenForAdmin(user.Username, user.Password, user.UUID, user.Role, jti, familyID, refreshExpiresAt)
if err != nil {
recordLoginLog(c, user.UUID, body.Username, 0, "生成刷新令牌失败")
authBaseController.HandleInternalError(c, "生成刷新令牌失败", err)
return
}
if err := refreshTokenSvc.Create(jti, familyID, user.UUID, "admin",
refreshExpiresAt, absoluteExpiresAt, c.Request.UserAgent(), c.ClientIP()); err != nil {
recordLoginLog(c, user.UUID, body.Username, 0, "持久化刷新令牌失败")
authBaseController.HandleInternalError(c, "持久化刷新令牌失败", err)
return
}
accessExpiresAt := time.Now().Add(time.Duration(settingsService.GetJWTExpire()) * time.Hour)
recordLoginLog(c, user.UUID, body.Username, 1, "登录成功")
authBaseController.HandleSuccess(c, "登录成功", gin.H{
"redirect": "/admin",
"avatar": user.Avatar,
"nickname": user.Nickname,
"username": user.Username,
"role": user.Role,
"token": token,
"accessToken": token,
"refreshToken": refreshToken,
"expires": accessExpiresAt,
})
}
// recordLoginLog 记录登录日志
// status: 1-成功, 0-失败
func recordLoginLog(c *gin.Context, uuid string, username string, status int, message string) {
db, err := database.GetDB()
if err != nil {
// 记录日志失败不应影响主流程,但可以记录到系统日志
fmt.Printf("Failed to connect to database for login log: %v\n", err)
return
}
log := models.LoginLog{
Type: "admin",
UUID: uuid,
Username: username,
IP: c.ClientIP(),
Status: status,
Message: "登录管理 - " + message,
UserAgent: c.Request.UserAgent(),
CreatedAt: time.Now(),
}
if err := db.Create(&log).Error; err != nil {
fmt.Printf("Failed to create login log: %v\n", err)
}
}
// LogoutHandler 管理员登出
// - 清理JWT Cookie会话
// - 撤销当前 refreshToken family
func LogoutHandler(c *gin.Context) {
// 尝试解析当前 access token提取 family通过 refresh DB 反查)
if token, err := getJWTCookie(c); err == nil && token != "" {
if claims, err := parseJWTToken(token); err == nil {
// access token 不带 family需通过 user uuid 撤销该用户全部活跃 refresh
revokeAllRefreshOfUser(claims.UUID)
}
}
// 清理JWT Cookie
clearInvalidJWTCookie(c)
authBaseController.HandleSuccess(c, "已退出登录", gin.H{
"redirect": "/admin/login",
})
}
// revokeAllRefreshOfUser 撤销该用户全部未撤销的 refreshToken
func revokeAllRefreshOfUser(userUUID string) {
db, err := database.GetDB()
if err != nil {
return
}
db.Model(&models.RefreshToken{}).
Where("user_uuid = ? AND user_type = ? AND revoked = ?", userUUID, "admin", false).
Update("revoked", true)
}
// RefreshTokenHandler 刷新管理员会话令牌
// - 校验请求体中的 refreshTokenOAuth2 风格)
// - DB 校验jti 存在、未撤销、未过期、未超绝对上限
// - 轮换:旧 jti 标记 revoked + replaced_by签发新 access + 新 refresh
// - 重用检测:旧已撤销 token 再次提交 -> 整 family 撤销
func RefreshTokenHandler(c *gin.Context) {
var body struct {
RefreshToken string `json:"refreshToken"`
}
_ = c.ShouldBindJSON(&body)
refreshTokenStr := strings.TrimSpace(body.RefreshToken)
if refreshTokenStr == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 1,
"msg": "缺少刷新令牌",
"data": nil,
})
return
}
// 1. 解析 JWT
claims, err := parseJWTToken(refreshTokenStr)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 1,
"msg": "无效的刷新令牌",
"data": nil,
})
return
}
// 2. 必须是 refresh 类型
if claims.TokenType != TokenTypeRefresh {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 1,
"msg": "令牌类型错误",
"data": nil,
})
return
}
// 3. DB 查询 jti
refreshSvc := services.GetRefreshTokenService()
rec, err := refreshSvc.FindByJTI(claims.ID)
if err != nil {
// 找不到 = 已被清理或伪造 -> 拒绝
c.JSON(http.StatusUnauthorized, gin.H{
"code": 1,
"msg": "刷新令牌不存在或已失效",
"data": nil,
})
return
}
// 4. 重用检测:已撤销的 token 被再次使用 -> 整族撤销
if rec.Revoked {
_ = refreshSvc.RevokeFamily(rec.FamilyID)
clearInvalidJWTCookie(c)
c.JSON(http.StatusUnauthorized, gin.H{
"code": 1,
"msg": "检测到刷新令牌重用,会话已强制失效",
"data": nil,
})
return
}
now := time.Now()
// 5. 过期检查
if now.After(rec.ExpiresAt) {
_ = refreshSvc.RevokeByJTI(rec.JTI)
c.JSON(http.StatusUnauthorized, gin.H{
"code": 1,
"msg": "刷新令牌已过期,请重新登录",
"data": nil,
})
return
}
// 6. 绝对上限检查
if now.After(rec.AbsoluteExpiresAt) {
_ = refreshSvc.RevokeFamily(rec.FamilyID)
c.JSON(http.StatusUnauthorized, gin.H{
"code": 1,
"msg": "会话已达最长有效期,请重新登录",
"data": nil,
})
return
}
// 7. 校验用户依然有效 + 密码未变
if !validateAdminPasswordHash(claims, c) {
_ = refreshSvc.RevokeFamily(rec.FamilyID)
clearInvalidJWTCookie(c)
c.JSON(http.StatusUnauthorized, gin.H{
"code": 1,
"msg": "会话已失效,请重新登录",
"data": nil,
})
return
}
db, err := database.GetDB()
if err != nil {
authBaseController.HandleInternalError(c, "数据库连接失败", err)
return
}
var adminUser models.User
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
_ = refreshSvc.RevokeFamily(rec.FamilyID)
clearInvalidJWTCookie(c)
c.JSON(http.StatusUnauthorized, gin.H{
"code": 1,
"msg": "会话已失效,请重新登录",
"data": nil,
})
return
}
// 8. 签发新 access + 新 refresh一次一换继承 absolute
settingsService := services.GetSettingsService()
newAccess, err := generateJWTTokenForAdmin(adminUser.Username, adminUser.Password, adminUser.UUID, adminUser.Role)
if err != nil {
authBaseController.HandleInternalError(c, "生成令牌失败", err)
return
}
refreshDays := settingsService.GetRefreshTokenExpireDays()
newRefreshExpiresAt := now.Add(time.Duration(refreshDays) * 24 * time.Hour)
if newRefreshExpiresAt.After(rec.AbsoluteExpiresAt) {
newRefreshExpiresAt = rec.AbsoluteExpiresAt
}
newJTI := refreshSvc.NewJTI()
newRefresh, err := generateRefreshTokenForAdmin(adminUser.Username, adminUser.Password, adminUser.UUID, adminUser.Role,
newJTI, rec.FamilyID, newRefreshExpiresAt)
if err != nil {
authBaseController.HandleInternalError(c, "生成刷新令牌失败", err)
return
}
if err := refreshSvc.Create(newJTI, rec.FamilyID, adminUser.UUID, "admin",
newRefreshExpiresAt, rec.AbsoluteExpiresAt, c.Request.UserAgent(), c.ClientIP()); err != nil {
authBaseController.HandleInternalError(c, "持久化刷新令牌失败", err)
return
}
if err := refreshSvc.Rotate(rec.JTI, newJTI); err != nil {
// 旋转失败不影响响应,但记录
logrus.WithError(err).Warn("rotate refresh token failed")
}
// 9. access token 通过响应体返回,不再同步到 Cookie
authBaseController.HandleSuccess(c, "刷新成功", gin.H{
"accessToken": newAccess,
"refreshToken": newRefresh,
"expires": now.Add(time.Duration(settingsService.GetJWTExpire()) * time.Hour),
})
}
// ============================================================================
// CSRF 相关辅助函数
// ============================================================================
const (
CSRFCookieName = "csrf_token"
CSRFHeaderName = "X-CSRF-Token"
CSRFFormField = "csrf_token"
)
// setCSRFToken 设置CSRF令牌到Cookie (Gin适配)
func setCSRFToken(c *gin.Context, token string) {
c.SetCookie(CSRFCookieName, token, 3600*24, "/", "", false, false)
c.Header(CSRFHeaderName, token)
}
// validateCSRFToken 验证CSRF令牌 (Gin适配)
func validateCSRFToken(c *gin.Context, requestToken string) bool {
// 获取Cookie中的令牌
cookie, err := c.Cookie(CSRFCookieName)
if err != nil || cookie == "" {
return false
}
cookieToken := cookie
// 如果请求体中没有提供token尝试从Header获取
if requestToken == "" {
requestToken = c.GetHeader(CSRFHeaderName)
}
if requestToken == "" {
return false
}
// 使用常量时间比较
return strings.Compare(cookieToken, requestToken) == 0
}
// ============================================================================
// 辅助函数
// ============================================================================
// clearInvalidJWTCookie 已废弃:当前使用纯 Bearer Token 模式,无需清理 Cookie
// 保留空实现以兼容历史调用
func clearInvalidJWTCookie(c *gin.Context) {
_ = c
}
// getJWTSecret 动态获取当前的JWT密钥
// 修复安全漏洞:确保每次都从最新配置中获取密钥,而不是使用启动时的全局变量
func getJWTSecret() []byte {
// 1. 尝试从数据库设置获取
settingsService := services.GetSettingsService()
if secret := settingsService.GetJWTSecret(); secret != "" {
return []byte(secret)
}
// 2. 尝试从配置文件获取(兼容旧配置)
if secret := viper.GetString("security.jwt_secret"); secret != "" {
return []byte(secret)
}
// 3. 如果仍未获取到,则记录严重错误并抛出 panic拒绝使用硬编码的不安全密钥
logrus.Fatal("致命安全错误: 无法获取有效的 JWT 密钥,请检查数据库设置或重新安装系统。系统拒绝以不安全模式运行。")
return nil
}
// ============================================================================
// 结构体定义
// ============================================================================
// Token 类型常量
const (
TokenTypeAccess = "access"
TokenTypeRefresh = "refresh"
)
// JWTClaims JWT载荷结构体
type JWTClaims struct {
Username string `json:"username"`
UUID string `json:"uuid"` // 用户UUID
Role int `json:"role"` // 用户角色
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
TokenType string `json:"typ,omitempty"` // access | refresh旧版无此字段视为 access
FamilyID string `json:"fid,omitempty"` // refresh 专用:会话族 ID
jwt.RegisteredClaims
}
// generateJWTTokenForAdmin 生成管理员 access JWT 令牌
// - 包含管理员用户名信息和密码哈希
// - 设置过期时间
// - 使用HMAC-SHA256签名
func generateJWTTokenForAdmin(username, passwordHash string, adminUUID string, role int) (string, error) {
passwordHashDigest := utils.GenerateSHA256Hash(passwordHash)
claims := JWTClaims{
Username: username,
UUID: adminUUID,
Role: role,
PasswordHash: passwordHashDigest,
TokenType: TokenTypeAccess,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(services.GetSettingsService().GetJWTExpire()) * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "NetworkAuth",
Subject: username,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(getJWTSecret())
}
// generateRefreshTokenForAdmin 生成管理员 refresh JWT 令牌
// - 携带 jti / family_id 用于持久化与轮换
// - 过期时间使用 settings.refresh_token_expire_days
func generateRefreshTokenForAdmin(username, passwordHash, adminUUID string, role int,
jti, familyID string, expiresAt time.Time) (string, error) {
passwordHashDigest := utils.GenerateSHA256Hash(passwordHash)
claims := JWTClaims{
Username: username,
UUID: adminUUID,
Role: role,
PasswordHash: passwordHashDigest,
TokenType: TokenTypeRefresh,
FamilyID: familyID,
RegisteredClaims: jwt.RegisteredClaims{
ID: jti,
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "NetworkAuth",
Subject: username,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(getJWTSecret())
}
// parseJWTToken 解析并验证JWT令牌
// - 验证签名有效性
// - 检查过期时间
// - 返回用户信息
func parseJWTToken(tokenString string) (*JWTClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return getJWTSecret(), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
// getJWTCookie 从 Authorization Bearer 头中读取 access token
// 注意:函数名保留以兼容历史调用,已不再读取 Cookie
func getJWTCookie(c *gin.Context) (string, error) {
authHeader := c.GetHeader("Authorization")
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
token := strings.TrimPrefix(authHeader, "Bearer ")
if token != "" {
return token, nil
}
}
return "", fmt.Errorf("未找到会话信息")
}
// validateAdminPasswordHash 验证管理员密码哈希的通用函数
func validateAdminPasswordHash(claims *JWTClaims, c *gin.Context) bool {
// 【安全修复】验证数据库中的当前密码哈希
// 这确保了密码修改后旧的JWT令牌会失效
db, err := database.GetDB()
if err != nil {
fmt.Printf("[SECURITY WARNING] Database connection failed during auth - Username=%s, IP=%s\n",
claims.Username, c.ClientIP())
return false
}
// 获取当前数据库中的管理员用户
var adminUser models.User
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
fmt.Printf("[SECURITY WARNING] Admin user not found in database - UUID=%s, IP=%s\n",
claims.UUID, c.ClientIP())
return false
}
// 检查账号状态 (Status=1 表示启用,否则强制下线)
if adminUser.Status != 1 {
fmt.Printf("[SECURITY WARNING] Admin user is disabled - UUID=%s, IP=%s\n",
claims.UUID, c.ClientIP())
return false
}
// 检查是否允许登录 (role=0 或 role=1 允许role=2不允许访问admin后台)
if adminUser.Role > 1 {
fmt.Printf("[SECURITY WARNING] Admin user role > 1 - UUID=%s, IP=%s\n",
claims.UUID, c.ClientIP())
return false
}
// 生成当前数据库密码的哈希摘要
currentPasswordHash := utils.GenerateSHA256Hash(adminUser.Password)
// 验证JWT中的密码哈希是否与当前数据库中的密码哈希一致
if claims.PasswordHash != currentPasswordHash {
fmt.Printf("[SECURITY WARNING] Password hash mismatch - JWT token invalidated - Username=%s, IP=%s\n",
claims.Username, c.ClientIP())
return false
}
return true
}
// IsAdminAuthenticated 判断管理员是否已认证Gin版本
// - 检查admin_session Cookie中的JWT令牌
// - 验证令牌签名、过期时间和用户角色
func IsAdminAuthenticated(c *gin.Context) bool {
cookie, err := getJWTCookie(c)
if err != nil || cookie == "" {
return false
}
// 解析并验证JWT令牌
claims, err := parseJWTToken(cookie)
if err != nil {
return false
}
// 验证密码哈希
return validateAdminPasswordHash(claims, c)
}
// IsAdminAuthenticatedHttp 判断管理员是否已认证HTTP兼容版本
// 保留此方法以兼容未迁移的 Handler
func IsAdminAuthenticatedHttp(r *http.Request) bool {
token := ""
cookie, err := r.Cookie("admin_session")
if err == nil && cookie.Value != "" {
token = cookie.Value
} else {
authHeader := r.Header.Get("Authorization")
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
token = strings.TrimPrefix(authHeader, "Bearer ")
}
}
if token == "" {
return false
}
// 解析并验证JWT令牌
claims, err := parseJWTToken(token)
if err != nil {
return false
}
// 注意HTTP 版本无法方便地获取 ClientIP 用于日志,且无法使用 Gin Context 的功能
// 这里仅做基本的 Token 验证。如果 Token 包含了 PasswordHash这里也会解析出来。
// 但验证 PasswordHash 需要 DB 访问。
// 为了完整性,我们应该也验证 PasswordHash。
// 这里的 ClientIP 只能从 r.RemoteAddr 获取。
db, err := database.GetDB()
if err != nil {
return false
}
var adminUser models.User
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
return false
}
// 检查账号状态 (Status=1 表示启用,否则强制下线)
if adminUser.Status != 1 {
return false
}
// 检查是否允许登录 (role=0 或 role=1 允许role=2不允许访问admin后台)
if adminUser.Role > 1 {
return false
}
// 验证密码哈希
currentPasswordHash := utils.GenerateSHA256Hash(adminUser.Password)
if claims.PasswordHash != currentPasswordHash {
return false
}
return true
}
// IsAdminAuthenticatedWithCleanup 带自动清理功能的JWT校验函数
// - 当JWT校验失败时自动清理失效的Cookie
// - 适用于API接口等需要清理失效令牌的场景
func IsAdminAuthenticatedWithCleanup(c *gin.Context) bool {
cookie, err := getJWTCookie(c)
if err != nil || cookie == "" {
return false
}
// 解析并验证JWT令牌
claims, err := parseJWTToken(cookie)
if err != nil {
// JWT解析失败清理失效Cookie
clearInvalidJWTCookie(c)
return false
}
// 验证密码哈希
if !validateAdminPasswordHash(claims, c) {
clearInvalidJWTCookie(c)
return false
}
return true
}
// GetCurrentAdminUser 获取当前登录的管理员用户信息 (HTTP 兼容版)
func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
token := ""
cookie, err := r.Cookie("admin_session")
if err == nil && cookie.Value != "" {
token = cookie.Value
} else {
authHeader := r.Header.Get("Authorization")
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
token = strings.TrimPrefix(authHeader, "Bearer ")
}
}
if token == "" {
return nil, fmt.Errorf("未找到会话信息")
}
claims, err := parseJWTToken(token)
if err != nil {
return nil, fmt.Errorf("无效的会话信息")
}
return claims, nil
}
// GetCurrentAdminUserWithRefresh 获取当前登录的管理员用户信息
// - 仅校验 access token 是否有效(不再做滑动续期)
// - 续期统一由前端调用 /refresh-token 完成OAuth2 风格)
// - 第二个返回值保留为 false 以兼容历史调用方
func GetCurrentAdminUserWithRefresh(c *gin.Context) (*JWTClaims, bool, error) {
cookie, err := getJWTCookie(c)
if err != nil {
return nil, false, fmt.Errorf("未找到会话信息")
}
claims, err := parseJWTToken(cookie)
if err != nil {
return nil, false, fmt.Errorf("无效的会话信息")
}
// access token 必须是 access 类型
if claims.TokenType != "" && claims.TokenType != TokenTypeAccess {
return nil, false, fmt.Errorf("令牌类型错误")
}
if !validateAdminPasswordHash(claims, c) {
return nil, false, fmt.Errorf("会话已失效,请重新登录")
}
return claims, false, nil
}
// AdminAuthRequired 管理员认证拦截中间件 (Gin Middleware)
// - 未登录:重定向到 /admin/login
// - 已登录:自动刷新接近过期的令牌,然后放行到后续处理器
func AdminAuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
// 尝试获取用户信息并自动刷新令牌
claims, refreshed, err := GetCurrentAdminUserWithRefresh(c)
if err != nil {
// 自动清理失效的JWT Cookie提升安全性和用户体验
clearInvalidJWTCookie(c)
// API 请求直接返回 401 JSON
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未登录或会话已过期",
"data": nil,
})
c.Abort()
return
}
// 如果令牌被刷新,可以在这里记录日志(可选)
if refreshed {
// 可以添加日志记录令牌刷新事件
_ = claims // 避免未使用变量警告
}
// 将解析出的用户信息存入上下文,供后续处理使用
c.Set("admin_uuid", claims.UUID)
c.Set("admin_username", claims.Username)
c.Set("admin_role", claims.Role)
c.Next()
}
}