2025-10-24 00:09:45 +08:00
|
|
|
|
package admin
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-18 21:51:17 +08:00
|
|
|
|
"NetworkAuth/controllers"
|
|
|
|
|
|
"NetworkAuth/database"
|
|
|
|
|
|
"NetworkAuth/models"
|
|
|
|
|
|
"NetworkAuth/services"
|
|
|
|
|
|
"NetworkAuth/utils"
|
2025-10-24 00:09:45 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
2025-10-26 14:48:02 +08:00
|
|
|
|
"github.com/gin-gonic/gin"
|
2025-10-24 00:09:45 +08:00
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
2026-04-04 20:50:45 +08:00
|
|
|
|
"github.com/sirupsen/logrus"
|
2025-10-24 00:09:45 +08:00
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-10-27 23:12:15 +08:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 全局变量
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
2025-10-26 14:48:02 +08:00
|
|
|
|
// 创建BaseController实例
|
|
|
|
|
|
var authBaseController = controllers.NewBaseController()
|
|
|
|
|
|
|
2025-10-27 23:12:15 +08:00
|
|
|
|
// ============================================================================
|
2026-03-28 23:30:02 +08:00
|
|
|
|
// API处理器
|
2025-10-27 23:12:15 +08:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
// CSRFTokenHandler 获取CSRF令牌接口
|
|
|
|
|
|
func CSRFTokenHandler(c *gin.Context) {
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// 尝试从Cookie获取
|
2026-03-28 23:30:02 +08:00
|
|
|
|
var token string
|
2026-03-18 21:51:17 +08:00
|
|
|
|
if cookie, err := c.Cookie(CSRFCookieName); err == nil && cookie != "" {
|
|
|
|
|
|
token = cookie
|
2025-10-26 03:05:27 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
newToken, err := utils.GenerateCSRFToken()
|
|
|
|
|
|
if err != nil {
|
2026-03-28 23:30:02 +08:00
|
|
|
|
authBaseController.HandleInternalError(c, "生成CSRF令牌失败", err)
|
2025-10-26 03:05:27 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
token = newToken
|
2026-03-18 21:51:17 +08:00
|
|
|
|
setCSRFToken(c, token)
|
2025-10-26 03:05:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
authBaseController.HandleSuccess(c, "success", gin.H{
|
|
|
|
|
|
"csrf_token": token,
|
|
|
|
|
|
})
|
2025-10-24 00:09:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// LoginHandler 管理员登录接口
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// - 接收JSON: {username, password, captcha, csrf_token}
|
|
|
|
|
|
// - 验证CSRF令牌
|
|
|
|
|
|
// - 验证验证码
|
2025-10-24 00:09:45 +08:00
|
|
|
|
// - 验证用户存在与密码正确性
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// - 仅允许管理员登录
|
|
|
|
|
|
// - 成功后设置JWT Cookie
|
2025-10-26 14:48:02 +08:00
|
|
|
|
func LoginHandler(c *gin.Context) {
|
2025-10-24 00:09:45 +08:00
|
|
|
|
var body struct {
|
2026-03-18 21:51:17 +08:00
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
|
Password string `json:"password"`
|
|
|
|
|
|
Captcha string `json:"captcha"`
|
|
|
|
|
|
CSRFToken string `json:"csrf_token"`
|
2025-10-24 00:09:45 +08:00
|
|
|
|
}
|
2025-10-27 23:12:15 +08:00
|
|
|
|
|
2025-10-26 14:48:02 +08:00
|
|
|
|
if !authBaseController.BindJSON(c, &body) {
|
2025-10-24 00:09:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-10-27 23:12:15 +08:00
|
|
|
|
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// 1. 验证CSRF令牌 (Gin 方式)
|
|
|
|
|
|
if !validateCSRFToken(c, body.CSRFToken) {
|
|
|
|
|
|
authBaseController.HandleValidationError(c, "CSRF令牌验证失败")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 14:48:02 +08:00
|
|
|
|
if !authBaseController.ValidateRequired(c, map[string]interface{}{
|
|
|
|
|
|
"用户名": body.Username,
|
|
|
|
|
|
"密码": body.Password,
|
|
|
|
|
|
"验证码": body.Captcha,
|
|
|
|
|
|
}) {
|
2025-10-24 03:08:43 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证验证码
|
2025-10-26 14:48:02 +08:00
|
|
|
|
if !VerifyCaptcha(c, body.Captcha) {
|
2026-04-04 20:50:45 +08:00
|
|
|
|
recordLoginLog(c, "", body.Username, 0, "验证码错误或已过期")
|
|
|
|
|
|
authBaseController.HandleValidationError(c, "验证码错误或已过期")
|
2025-10-24 03:08:43 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-10-24 00:09:45 +08:00
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
// 从数据库中查找对应的用户
|
|
|
|
|
|
db, err := database.GetDB()
|
|
|
|
|
|
if err != nil {
|
2026-04-04 20:50:45 +08:00
|
|
|
|
recordLoginLog(c, "", body.Username, 0, "数据库连接失败")
|
2026-03-28 23:30:02 +08:00
|
|
|
|
authBaseController.HandleInternalError(c, "数据库连接失败", err)
|
2025-10-26 09:35:07 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
var user models.User
|
2026-04-04 20:50:45 +08:00
|
|
|
|
if err := db.Where("username = ?", body.Username).First(&user).Error; err != nil {
|
|
|
|
|
|
recordLoginLog(c, user.UUID, body.Username, 0, "用户不存在")
|
2025-10-26 14:48:02 +08:00
|
|
|
|
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
2025-10-26 09:35:07 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-04 20:50:45 +08:00
|
|
|
|
// 检查账号状态 (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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// 验证密码(使用盐值校验)
|
2026-03-28 23:30:02 +08:00
|
|
|
|
if !utils.VerifyPasswordWithSalt(body.Password, user.PasswordSalt, user.Password) {
|
2026-04-04 20:50:45 +08:00
|
|
|
|
recordLoginLog(c, user.UUID, body.Username, 0, "密码错误")
|
2025-10-26 14:48:02 +08:00
|
|
|
|
authBaseController.HandleValidationError(c, "用户不存在或密码错误")
|
2025-10-24 00:09:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// 生成 access JWT
|
2026-04-04 20:50:45 +08:00
|
|
|
|
token, err := generateJWTTokenForAdmin(user.Username, user.Password, user.UUID, user.Role)
|
2025-10-24 00:09:45 +08:00
|
|
|
|
if err != nil {
|
2026-04-04 20:50:45 +08:00
|
|
|
|
recordLoginLog(c, user.UUID, body.Username, 0, "生成令牌失败")
|
2025-10-26 14:48:02 +08:00
|
|
|
|
authBaseController.HandleInternalError(c, "生成令牌失败", err)
|
2025-10-24 00:09:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// 签发 refreshToken(新 family)
|
2026-03-28 23:30:02 +08:00
|
|
|
|
settingsService := services.GetSettingsService()
|
2026-05-04 22:02:26 +08:00
|
|
|
|
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)
|
2025-10-24 00:09:45 +08:00
|
|
|
|
|
2026-04-04 20:50:45 +08:00
|
|
|
|
recordLoginLog(c, user.UUID, body.Username, 1, "登录成功")
|
2025-10-26 14:48:02 +08:00
|
|
|
|
authBaseController.HandleSuccess(c, "登录成功", gin.H{
|
2026-05-04 22:02:26 +08:00
|
|
|
|
"redirect": "/admin",
|
|
|
|
|
|
"avatar": user.Avatar,
|
|
|
|
|
|
"nickname": user.Nickname,
|
|
|
|
|
|
"username": user.Username,
|
|
|
|
|
|
"role": user.Role,
|
|
|
|
|
|
"token": token,
|
|
|
|
|
|
"accessToken": token,
|
|
|
|
|
|
"refreshToken": refreshToken,
|
|
|
|
|
|
"expires": accessExpiresAt,
|
2025-10-24 00:09:45 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// recordLoginLog 记录登录日志
|
|
|
|
|
|
// status: 1-成功, 0-失败
|
2026-04-04 20:50:45 +08:00
|
|
|
|
func recordLoginLog(c *gin.Context, uuid string, username string, status int, message string) {
|
2026-03-18 21:51:17 +08:00
|
|
|
|
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",
|
2026-04-04 20:50:45 +08:00
|
|
|
|
UUID: uuid,
|
2026-03-18 21:51:17 +08:00
|
|
|
|
Username: username,
|
|
|
|
|
|
IP: c.ClientIP(),
|
|
|
|
|
|
Status: status,
|
2026-04-04 20:50:45 +08:00
|
|
|
|
Message: "登录管理 - " + message,
|
2026-03-18 21:51:17 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-24 00:09:45 +08:00
|
|
|
|
// LogoutHandler 管理员登出
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// - 清理JWT Cookie会话
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// - 撤销当前 refreshToken family
|
2025-10-26 14:48:02 +08:00
|
|
|
|
func LogoutHandler(c *gin.Context) {
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// 尝试解析当前 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-24 00:09:45 +08:00
|
|
|
|
// 清理JWT Cookie
|
2025-10-26 14:48:02 +08:00
|
|
|
|
clearInvalidJWTCookie(c)
|
2025-10-26 01:51:25 +08:00
|
|
|
|
|
2025-10-26 14:48:02 +08:00
|
|
|
|
authBaseController.HandleSuccess(c, "已退出登录", gin.H{
|
2025-10-26 01:51:25 +08:00
|
|
|
|
"redirect": "/admin/login",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 11:29:19 +08:00
|
|
|
|
// RefreshTokenHandler 刷新管理员会话令牌
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// - 校验请求体中的 refreshToken(OAuth2 风格)
|
|
|
|
|
|
// - DB 校验:jti 存在、未撤销、未过期、未超绝对上限
|
|
|
|
|
|
// - 轮换:旧 jti 标记 revoked + replaced_by;签发新 access + 新 refresh
|
|
|
|
|
|
// - 重用检测:旧已撤销 token 再次提交 -> 整 family 撤销
|
2026-05-04 11:29:19 +08:00
|
|
|
|
func RefreshTokenHandler(c *gin.Context) {
|
2026-05-04 22:02:26 +08:00
|
|
|
|
var body struct {
|
|
|
|
|
|
RefreshToken string `json:"refreshToken"`
|
|
|
|
|
|
}
|
|
|
|
|
|
_ = c.ShouldBindJSON(&body)
|
|
|
|
|
|
refreshTokenStr := strings.TrimSpace(body.RefreshToken)
|
|
|
|
|
|
if refreshTokenStr == "" {
|
2026-05-04 11:29:19 +08:00
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{
|
|
|
|
|
|
"code": 1,
|
2026-05-04 22:02:26 +08:00
|
|
|
|
"msg": "缺少刷新令牌",
|
2026-05-04 11:29:19 +08:00
|
|
|
|
"data": nil,
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// 1. 解析 JWT
|
|
|
|
|
|
claims, err := parseJWTToken(refreshTokenStr)
|
2026-05-04 11:29:19 +08:00
|
|
|
|
if err != nil {
|
2026-05-04 22:02:26 +08:00
|
|
|
|
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)
|
2026-05-04 11:29:19 +08:00
|
|
|
|
clearInvalidJWTCookie(c)
|
|
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{
|
|
|
|
|
|
"code": 1,
|
2026-05-04 22:02:26 +08:00
|
|
|
|
"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": "会话已达最长有效期,请重新登录",
|
2026-05-04 11:29:19 +08:00
|
|
|
|
"data": nil,
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// 7. 校验用户依然有效 + 密码未变
|
2026-05-04 11:29:19 +08:00
|
|
|
|
if !validateAdminPasswordHash(claims, c) {
|
2026-05-04 22:02:26 +08:00
|
|
|
|
_ = refreshSvc.RevokeFamily(rec.FamilyID)
|
2026-05-04 11:29:19 +08:00
|
|
|
|
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 {
|
2026-05-04 22:02:26 +08:00
|
|
|
|
_ = refreshSvc.RevokeFamily(rec.FamilyID)
|
2026-05-04 11:29:19 +08:00
|
|
|
|
clearInvalidJWTCookie(c)
|
|
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{
|
|
|
|
|
|
"code": 1,
|
|
|
|
|
|
"msg": "会话已失效,请重新登录",
|
|
|
|
|
|
"data": nil,
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// 8. 签发新 access + 新 refresh(一次一换,继承 absolute)
|
|
|
|
|
|
settingsService := services.GetSettingsService()
|
|
|
|
|
|
newAccess, err := generateJWTTokenForAdmin(adminUser.Username, adminUser.Password, adminUser.UUID, adminUser.Role)
|
2026-05-04 11:29:19 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
authBaseController.HandleInternalError(c, "生成令牌失败", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-04 22:02:26 +08:00
|
|
|
|
refreshDays := settingsService.GetRefreshTokenExpireDays()
|
|
|
|
|
|
newRefreshExpiresAt := now.Add(time.Duration(refreshDays) * 24 * time.Hour)
|
|
|
|
|
|
if newRefreshExpiresAt.After(rec.AbsoluteExpiresAt) {
|
|
|
|
|
|
newRefreshExpiresAt = rec.AbsoluteExpiresAt
|
2026-05-04 11:29:19 +08:00
|
|
|
|
}
|
2026-05-04 22:02:26 +08:00
|
|
|
|
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
|
2026-05-04 11:29:19 +08:00
|
|
|
|
|
|
|
|
|
|
authBaseController.HandleSuccess(c, "刷新成功", gin.H{
|
2026-05-04 22:02:26 +08:00
|
|
|
|
"accessToken": newAccess,
|
|
|
|
|
|
"refreshToken": newRefresh,
|
|
|
|
|
|
"expires": now.Add(time.Duration(settingsService.GetJWTExpire()) * time.Hour),
|
2026-05-04 11:29:19 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-27 23:12:15 +08:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 辅助函数
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// clearInvalidJWTCookie 已废弃:当前使用纯 Bearer Token 模式,无需清理 Cookie
|
|
|
|
|
|
// 保留空实现以兼容历史调用
|
2025-10-26 14:48:02 +08:00
|
|
|
|
func clearInvalidJWTCookie(c *gin.Context) {
|
2026-05-04 22:02:26 +08:00
|
|
|
|
_ = c
|
2025-10-24 00:09:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 11:57:31 +08:00
|
|
|
|
// getJWTSecret 动态获取当前的JWT密钥
|
|
|
|
|
|
// 修复安全漏洞:确保每次都从最新配置中获取密钥,而不是使用启动时的全局变量
|
|
|
|
|
|
func getJWTSecret() []byte {
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// 1. 尝试从数据库设置获取
|
|
|
|
|
|
settingsService := services.GetSettingsService()
|
|
|
|
|
|
if secret := settingsService.GetJWTSecret(); secret != "" {
|
|
|
|
|
|
return []byte(secret)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 尝试从配置文件获取(兼容旧配置)
|
|
|
|
|
|
if secret := viper.GetString("security.jwt_secret"); secret != "" {
|
|
|
|
|
|
return []byte(secret)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-04 20:50:45 +08:00
|
|
|
|
// 3. 如果仍未获取到,则记录严重错误并抛出 panic,拒绝使用硬编码的不安全密钥
|
|
|
|
|
|
logrus.Fatal("致命安全错误: 无法获取有效的 JWT 密钥,请检查数据库设置或重新安装系统。系统拒绝以不安全模式运行。")
|
|
|
|
|
|
return nil
|
2025-10-26 11:57:31 +08:00
|
|
|
|
}
|
2025-10-24 00:09:45 +08:00
|
|
|
|
|
2025-10-27 23:12:15 +08:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 结构体定义
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// Token 类型常量
|
|
|
|
|
|
const (
|
|
|
|
|
|
TokenTypeAccess = "access"
|
|
|
|
|
|
TokenTypeRefresh = "refresh"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-10-27 23:12:15 +08:00
|
|
|
|
// JWTClaims JWT载荷结构体
|
2025-10-24 00:09:45 +08:00
|
|
|
|
type JWTClaims struct {
|
2025-10-26 01:51:25 +08:00
|
|
|
|
Username string `json:"username"`
|
2026-04-04 20:50:45 +08:00
|
|
|
|
UUID string `json:"uuid"` // 用户UUID
|
|
|
|
|
|
Role int `json:"role"` // 用户角色
|
2025-10-26 01:51:25 +08:00
|
|
|
|
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
|
2026-05-04 22:02:26 +08:00
|
|
|
|
TokenType string `json:"typ,omitempty"` // access | refresh,旧版无此字段视为 access
|
|
|
|
|
|
FamilyID string `json:"fid,omitempty"` // refresh 专用:会话族 ID
|
2025-10-24 00:09:45 +08:00
|
|
|
|
jwt.RegisteredClaims
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// generateJWTTokenForAdmin 生成管理员 access JWT 令牌
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// - 包含管理员用户名信息和密码哈希
|
|
|
|
|
|
// - 设置过期时间
|
2025-10-24 00:09:45 +08:00
|
|
|
|
// - 使用HMAC-SHA256签名
|
2026-04-04 20:50:45 +08:00
|
|
|
|
func generateJWTTokenForAdmin(username, passwordHash string, adminUUID string, role int) (string, error) {
|
2026-03-18 21:51:17 +08:00
|
|
|
|
passwordHashDigest := utils.GenerateSHA256Hash(passwordHash)
|
|
|
|
|
|
|
2025-10-24 00:09:45 +08:00
|
|
|
|
claims := JWTClaims{
|
2026-03-18 21:51:17 +08:00
|
|
|
|
Username: username,
|
|
|
|
|
|
UUID: adminUUID,
|
2026-05-04 22:02:26 +08:00
|
|
|
|
Role: role,
|
|
|
|
|
|
PasswordHash: passwordHashDigest,
|
|
|
|
|
|
TokenType: TokenTypeAccess,
|
2025-10-24 00:09:45 +08:00
|
|
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
2026-03-18 21:51:17 +08:00
|
|
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(services.GetSettingsService().GetJWTExpire()) * time.Hour)),
|
2025-10-24 00:09:45 +08:00
|
|
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
|
|
|
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
2026-03-18 21:51:17 +08:00
|
|
|
|
Issuer: "NetworkAuth",
|
|
|
|
|
|
Subject: username,
|
2025-10-24 00:09:45 +08:00
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
2025-10-26 11:57:31 +08:00
|
|
|
|
return token.SignedString(getJWTSecret())
|
2025-10-24 00:09:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// 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())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-24 00:09:45 +08:00
|
|
|
|
// 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"])
|
|
|
|
|
|
}
|
2025-10-26 11:57:31 +08:00
|
|
|
|
return getJWTSecret(), nil
|
2025-10-24 00:09:45 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
|
|
|
|
|
|
return claims, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("invalid token")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// getJWTCookie 从 Authorization Bearer 头中读取 access token
|
|
|
|
|
|
// 注意:函数名保留以兼容历史调用,已不再读取 Cookie
|
2025-10-26 14:48:02 +08:00
|
|
|
|
func getJWTCookie(c *gin.Context) (string, error) {
|
2026-03-31 02:14:08 +08:00
|
|
|
|
authHeader := c.GetHeader("Authorization")
|
|
|
|
|
|
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
|
|
|
|
|
|
token := strings.TrimPrefix(authHeader, "Bearer ")
|
2026-05-04 22:02:26 +08:00
|
|
|
|
if token != "" {
|
|
|
|
|
|
return token, nil
|
|
|
|
|
|
}
|
2026-03-31 02:14:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
return "", fmt.Errorf("未找到会话信息")
|
2025-10-26 11:57:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// validateAdminPasswordHash 验证管理员密码哈希的通用函数
|
2025-10-26 14:48:02 +08:00
|
|
|
|
func validateAdminPasswordHash(claims *JWTClaims, c *gin.Context) bool {
|
2025-10-26 11:57:31 +08:00
|
|
|
|
// 【安全修复】验证数据库中的当前密码哈希
|
|
|
|
|
|
// 这确保了密码修改后,旧的JWT令牌会失效
|
|
|
|
|
|
db, err := database.GetDB()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
fmt.Printf("[SECURITY WARNING] Database connection failed during auth - Username=%s, IP=%s\n",
|
2025-10-26 14:48:02 +08:00
|
|
|
|
claims.Username, c.ClientIP())
|
2025-10-26 11:57:31 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
// 获取当前数据库中的管理员用户
|
|
|
|
|
|
var adminUser models.User
|
2026-04-04 20:50:45 +08:00
|
|
|
|
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())
|
2025-10-26 11:57:31 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成当前数据库密码的哈希摘要
|
2026-03-28 23:30:02 +08:00
|
|
|
|
currentPasswordHash := utils.GenerateSHA256Hash(adminUser.Password)
|
2025-10-26 11:57:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 验证JWT中的密码哈希是否与当前数据库中的密码哈希一致
|
|
|
|
|
|
if claims.PasswordHash != currentPasswordHash {
|
|
|
|
|
|
fmt.Printf("[SECURITY WARNING] Password hash mismatch - JWT token invalidated - Username=%s, IP=%s\n",
|
2025-10-26 14:48:02 +08:00
|
|
|
|
claims.Username, c.ClientIP())
|
2025-10-26 11:57:31 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// IsAdminAuthenticated 判断管理员是否已认证(Gin版本)
|
2025-10-24 00:09:45 +08:00
|
|
|
|
// - 检查admin_session Cookie中的JWT令牌
|
|
|
|
|
|
// - 验证令牌签名、过期时间和用户角色
|
2025-10-26 14:48:02 +08:00
|
|
|
|
func IsAdminAuthenticated(c *gin.Context) bool {
|
|
|
|
|
|
cookie, err := getJWTCookie(c)
|
|
|
|
|
|
if err != nil || cookie == "" {
|
2025-10-24 00:09:45 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析并验证JWT令牌
|
2025-10-26 14:48:02 +08:00
|
|
|
|
claims, err := parseJWTToken(cookie)
|
2025-10-24 00:09:45 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 11:57:31 +08:00
|
|
|
|
// 验证密码哈希
|
2025-10-26 14:48:02 +08:00
|
|
|
|
return validateAdminPasswordHash(claims, c)
|
2025-10-26 01:51:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// IsAdminAuthenticatedHttp 判断管理员是否已认证(HTTP兼容版本)
|
|
|
|
|
|
// 保留此方法以兼容未迁移的 Handler
|
|
|
|
|
|
func IsAdminAuthenticatedHttp(r *http.Request) bool {
|
2026-03-31 02:14:08 +08:00
|
|
|
|
token := ""
|
2026-03-18 21:51:17 +08:00
|
|
|
|
cookie, err := r.Cookie("admin_session")
|
2026-03-31 02:14:08 +08:00
|
|
|
|
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 == "" {
|
2026-03-18 21:51:17 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析并验证JWT令牌
|
2026-03-31 02:14:08 +08:00
|
|
|
|
claims, err := parseJWTToken(token)
|
2026-03-18 21:51:17 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
var adminUser models.User
|
2026-04-04 20:50:45 +08:00
|
|
|
|
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 {
|
2026-03-18 21:51:17 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
// 验证密码哈希
|
|
|
|
|
|
currentPasswordHash := utils.GenerateSHA256Hash(adminUser.Password)
|
2026-03-18 21:51:17 +08:00
|
|
|
|
if claims.PasswordHash != currentPasswordHash {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 01:51:25 +08:00
|
|
|
|
// IsAdminAuthenticatedWithCleanup 带自动清理功能的JWT校验函数
|
|
|
|
|
|
// - 当JWT校验失败时,自动清理失效的Cookie
|
|
|
|
|
|
// - 适用于API接口等需要清理失效令牌的场景
|
2025-10-26 14:48:02 +08:00
|
|
|
|
func IsAdminAuthenticatedWithCleanup(c *gin.Context) bool {
|
|
|
|
|
|
cookie, err := getJWTCookie(c)
|
|
|
|
|
|
if err != nil || cookie == "" {
|
2025-10-26 01:51:25 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析并验证JWT令牌
|
2025-10-26 14:48:02 +08:00
|
|
|
|
claims, err := parseJWTToken(cookie)
|
2025-10-26 01:51:25 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
// JWT解析失败,清理失效Cookie
|
2025-10-26 14:48:02 +08:00
|
|
|
|
clearInvalidJWTCookie(c)
|
2025-10-26 01:51:25 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 11:57:31 +08:00
|
|
|
|
// 验证密码哈希
|
2025-10-26 14:48:02 +08:00
|
|
|
|
if !validateAdminPasswordHash(claims, c) {
|
|
|
|
|
|
clearInvalidJWTCookie(c)
|
2025-10-26 01:51:25 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
2025-10-24 00:09:45 +08:00
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// GetCurrentAdminUser 获取当前登录的管理员用户信息 (HTTP 兼容版)
|
|
|
|
|
|
func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
|
2026-03-31 02:14:08 +08:00
|
|
|
|
token := ""
|
2026-03-18 21:51:17 +08:00
|
|
|
|
cookie, err := r.Cookie("admin_session")
|
2026-03-31 02:14:08 +08:00
|
|
|
|
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 == "" {
|
2025-10-24 00:09:45 +08:00
|
|
|
|
return nil, fmt.Errorf("未找到会话信息")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 02:14:08 +08:00
|
|
|
|
claims, err := parseJWTToken(token)
|
2025-10-24 00:09:45 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("无效的会话信息")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return claims, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// GetCurrentAdminUserWithRefresh 获取当前登录的管理员用户信息
|
|
|
|
|
|
// - 仅校验 access token 是否有效(不再做滑动续期)
|
|
|
|
|
|
// - 续期统一由前端调用 /refresh-token 完成(OAuth2 风格)
|
|
|
|
|
|
// - 第二个返回值保留为 false 以兼容历史调用方
|
2025-10-26 14:48:02 +08:00
|
|
|
|
func GetCurrentAdminUserWithRefresh(c *gin.Context) (*JWTClaims, bool, error) {
|
|
|
|
|
|
cookie, err := getJWTCookie(c)
|
2025-10-24 00:09:45 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, false, fmt.Errorf("未找到会话信息")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 14:48:02 +08:00
|
|
|
|
claims, err := parseJWTToken(cookie)
|
2025-10-24 00:09:45 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, false, fmt.Errorf("无效的会话信息")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
// access token 必须是 access 类型
|
|
|
|
|
|
if claims.TokenType != "" && claims.TokenType != TokenTypeAccess {
|
|
|
|
|
|
return nil, false, fmt.Errorf("令牌类型错误")
|
2026-03-18 21:51:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
if !validateAdminPasswordHash(claims, c) {
|
|
|
|
|
|
return nil, false, fmt.Errorf("会话已失效,请重新登录")
|
2026-03-18 21:51:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-04 22:02:26 +08:00
|
|
|
|
return claims, false, nil
|
2025-10-24 00:09:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// AdminAuthRequired 管理员认证拦截中间件 (Gin Middleware)
|
2025-10-24 00:09:45 +08:00
|
|
|
|
// - 未登录:重定向到 /admin/login
|
|
|
|
|
|
// - 已登录:自动刷新接近过期的令牌,然后放行到后续处理器
|
2025-10-26 14:48:02 +08:00
|
|
|
|
func AdminAuthRequired() gin.HandlerFunc {
|
|
|
|
|
|
return func(c *gin.Context) {
|
2025-10-24 00:09:45 +08:00
|
|
|
|
// 尝试获取用户信息并自动刷新令牌
|
2025-10-26 14:48:02 +08:00
|
|
|
|
claims, refreshed, err := GetCurrentAdminUserWithRefresh(c)
|
2025-10-24 00:09:45 +08:00
|
|
|
|
if err != nil {
|
2025-10-26 01:51:25 +08:00
|
|
|
|
// 自动清理失效的JWT Cookie,提升安全性和用户体验
|
2025-10-26 14:48:02 +08:00
|
|
|
|
clearInvalidJWTCookie(c)
|
2025-10-26 09:35:07 +08:00
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
// API 请求直接返回 401 JSON
|
|
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{
|
|
|
|
|
|
"success": false,
|
|
|
|
|
|
"message": "未登录或会话已过期",
|
|
|
|
|
|
"data": nil,
|
|
|
|
|
|
})
|
2025-10-26 14:48:02 +08:00
|
|
|
|
c.Abort()
|
2025-10-24 00:09:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果令牌被刷新,可以在这里记录日志(可选)
|
|
|
|
|
|
if refreshed {
|
|
|
|
|
|
// 可以添加日志记录令牌刷新事件
|
|
|
|
|
|
_ = claims // 避免未使用变量警告
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// 将解析出的用户信息存入上下文,供后续处理使用
|
|
|
|
|
|
c.Set("admin_uuid", claims.UUID)
|
|
|
|
|
|
c.Set("admin_username", claims.Username)
|
|
|
|
|
|
c.Set("admin_role", claims.Role)
|
|
|
|
|
|
|
2025-10-26 14:48:02 +08:00
|
|
|
|
c.Next()
|
2025-10-24 00:09:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|