mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
调整 修改认证方式为 OAuth2 鉴权
This commit is contained in:
@@ -125,7 +125,7 @@ func LoginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成JWT令牌
|
// 生成 access JWT
|
||||||
token, err := generateJWTTokenForAdmin(user.Username, user.Password, user.UUID, user.Role)
|
token, err := generateJWTTokenForAdmin(user.Username, user.Password, user.UUID, user.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordLoginLog(c, user.UUID, body.Username, 0, "生成令牌失败")
|
recordLoginLog(c, user.UUID, body.Username, 0, "生成令牌失败")
|
||||||
@@ -133,12 +133,32 @@ func LoginHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置JWT Cookie(HttpOnly,安全)
|
// 签发 refreshToken(新 family)
|
||||||
// 使用系统配置的Cookie参数
|
|
||||||
settingsService := services.GetSettingsService()
|
settingsService := services.GetSettingsService()
|
||||||
secure, sameSite, domain, maxAge := settingsService.GetCookieConfig()
|
refreshTokenSvc := services.GetRefreshTokenService()
|
||||||
cookie := utils.CreateSecureCookie("admin_session", token, maxAge, domain, secure, sameSite)
|
refreshDays := settingsService.GetRefreshTokenExpireDays()
|
||||||
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
|
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, "登录成功")
|
recordLoginLog(c, user.UUID, body.Username, 1, "登录成功")
|
||||||
authBaseController.HandleSuccess(c, "登录成功", gin.H{
|
authBaseController.HandleSuccess(c, "登录成功", gin.H{
|
||||||
@@ -148,6 +168,9 @@ func LoginHandler(c *gin.Context) {
|
|||||||
"username": user.Username,
|
"username": user.Username,
|
||||||
"role": user.Role,
|
"role": user.Role,
|
||||||
"token": token,
|
"token": token,
|
||||||
|
"accessToken": token,
|
||||||
|
"refreshToken": refreshToken,
|
||||||
|
"expires": accessExpiresAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,8 +202,16 @@ func recordLoginLog(c *gin.Context, uuid string, username string, status int, me
|
|||||||
|
|
||||||
// LogoutHandler 管理员登出
|
// LogoutHandler 管理员登出
|
||||||
// - 清理JWT Cookie会话
|
// - 清理JWT Cookie会话
|
||||||
// - 确保令牌完全失效
|
// - 撤销当前 refreshToken family
|
||||||
func LogoutHandler(c *gin.Context) {
|
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
|
// 清理JWT Cookie
|
||||||
clearInvalidJWTCookie(c)
|
clearInvalidJWTCookie(c)
|
||||||
|
|
||||||
@@ -189,34 +220,110 @@ func LogoutHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshTokenHandler 刷新管理员会话令牌
|
// revokeAllRefreshOfUser 撤销该用户全部未撤销的 refreshToken
|
||||||
// - 校验当前会话(Cookie 或 Authorization)
|
func revokeAllRefreshOfUser(userUUID string) {
|
||||||
// - 重新签发 JWT 并同步写回 Cookie
|
db, err := database.GetDB()
|
||||||
// - 返回前端可直接持久化的新 token 信息
|
|
||||||
func RefreshTokenHandler(c *gin.Context) {
|
|
||||||
token, err := getJWTCookie(c)
|
|
||||||
if err != nil || token == "" {
|
|
||||||
clearInvalidJWTCookie(c)
|
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{
|
|
||||||
"code": 1,
|
|
||||||
"msg": "未登录或会话已过期",
|
|
||||||
"data": nil,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, err := parseJWTToken(token)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
clearInvalidJWTCookie(c)
|
return
|
||||||
|
}
|
||||||
|
db.Model(&models.RefreshToken{}).
|
||||||
|
Where("user_uuid = ? AND user_type = ? AND revoked = ?", userUUID, "admin", false).
|
||||||
|
Update("revoked", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshTokenHandler 刷新管理员会话令牌
|
||||||
|
// - 校验请求体中的 refreshToken(OAuth2 风格)
|
||||||
|
// - 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{
|
c.JSON(http.StatusUnauthorized, gin.H{
|
||||||
"code": 1,
|
"code": 1,
|
||||||
"msg": "无效的会话信息",
|
"msg": "缺少刷新令牌",
|
||||||
"data": nil,
|
"data": nil,
|
||||||
})
|
})
|
||||||
return
|
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) {
|
if !validateAdminPasswordHash(claims, c) {
|
||||||
|
_ = refreshSvc.RevokeFamily(rec.FamilyID)
|
||||||
clearInvalidJWTCookie(c)
|
clearInvalidJWTCookie(c)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{
|
c.JSON(http.StatusUnauthorized, gin.H{
|
||||||
"code": 1,
|
"code": 1,
|
||||||
@@ -231,9 +338,9 @@ func RefreshTokenHandler(c *gin.Context) {
|
|||||||
authBaseController.HandleInternalError(c, "数据库连接失败", err)
|
authBaseController.HandleInternalError(c, "数据库连接失败", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var adminUser models.User
|
var adminUser models.User
|
||||||
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
|
if err := db.Where("uuid = ?", claims.UUID).First(&adminUser).Error; err != nil {
|
||||||
|
_ = refreshSvc.RevokeFamily(rec.FamilyID)
|
||||||
clearInvalidJWTCookie(c)
|
clearInvalidJWTCookie(c)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{
|
c.JSON(http.StatusUnauthorized, gin.H{
|
||||||
"code": 1,
|
"code": 1,
|
||||||
@@ -243,25 +350,41 @@ func RefreshTokenHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newToken, err := generateJWTTokenForAdmin(adminUser.Username, adminUser.Password, adminUser.UUID, adminUser.Role)
|
// 8. 签发新 access + 新 refresh(一次一换,继承 absolute)
|
||||||
|
settingsService := services.GetSettingsService()
|
||||||
|
newAccess, err := generateJWTTokenForAdmin(adminUser.Username, adminUser.Password, adminUser.UUID, adminUser.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
authBaseController.HandleInternalError(c, "生成令牌失败", err)
|
authBaseController.HandleInternalError(c, "生成令牌失败", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
refreshDays := settingsService.GetRefreshTokenExpireDays()
|
||||||
secure, sameSite, domain, maxAge := services.GetSettingsService().GetCookieConfig()
|
newRefreshExpiresAt := now.Add(time.Duration(refreshDays) * 24 * time.Hour)
|
||||||
cookieObj := utils.CreateSecureCookie("admin_session", newToken, maxAge, domain, secure, sameSite)
|
if newRefreshExpiresAt.After(rec.AbsoluteExpiresAt) {
|
||||||
c.SetCookie(cookieObj.Name, cookieObj.Value, cookieObj.MaxAge, cookieObj.Path, cookieObj.Domain, cookieObj.Secure, cookieObj.HttpOnly)
|
newRefreshExpiresAt = rec.AbsoluteExpiresAt
|
||||||
|
}
|
||||||
expireHours := services.GetSettingsService().GetJWTExpire()
|
newJTI := refreshSvc.NewJTI()
|
||||||
if expireHours <= 0 {
|
newRefresh, err := generateRefreshTokenForAdmin(adminUser.Username, adminUser.Password, adminUser.UUID, adminUser.Role,
|
||||||
expireHours = 24
|
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{
|
authBaseController.HandleSuccess(c, "刷新成功", gin.H{
|
||||||
"accessToken": newToken,
|
"accessToken": newAccess,
|
||||||
"refreshToken": newToken,
|
"refreshToken": newRefresh,
|
||||||
"expires": time.Now().Add(time.Duration(expireHours) * time.Hour),
|
"expires": now.Add(time.Duration(settingsService.GetJWTExpire()) * time.Hour),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,13 +430,10 @@ func validateCSRFToken(c *gin.Context, requestToken string) bool {
|
|||||||
// 辅助函数
|
// 辅助函数
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// clearInvalidJWTCookie 清理无效的JWT Cookie
|
// clearInvalidJWTCookie 已废弃:当前使用纯 Bearer Token 模式,无需清理 Cookie
|
||||||
// - 统一的Cookie清理函数,确保一致性
|
// 保留空实现以兼容历史调用
|
||||||
// - 在JWT校验失败时自动调用,提升安全性和用户体验
|
|
||||||
func clearInvalidJWTCookie(c *gin.Context) {
|
func clearInvalidJWTCookie(c *gin.Context) {
|
||||||
_, _, domain, _ := services.GetSettingsService().GetCookieConfig()
|
_ = c
|
||||||
cookie := utils.CreateExpiredCookie("admin_session", domain)
|
|
||||||
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getJWTSecret 动态获取当前的JWT密钥
|
// getJWTSecret 动态获取当前的JWT密钥
|
||||||
@@ -339,34 +459,36 @@ func getJWTSecret() []byte {
|
|||||||
// 结构体定义
|
// 结构体定义
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
// Token 类型常量
|
||||||
|
const (
|
||||||
|
TokenTypeAccess = "access"
|
||||||
|
TokenTypeRefresh = "refresh"
|
||||||
|
)
|
||||||
|
|
||||||
// JWTClaims JWT载荷结构体
|
// JWTClaims JWT载荷结构体
|
||||||
type JWTClaims struct {
|
type JWTClaims struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
UUID string `json:"uuid"` // 用户UUID
|
UUID string `json:"uuid"` // 用户UUID
|
||||||
Role int `json:"role"` // 用户角色
|
Role int `json:"role"` // 用户角色
|
||||||
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
|
PasswordHash string `json:"password_hash"` // 密码哈希摘要,用于验证密码是否被修改
|
||||||
|
TokenType string `json:"typ,omitempty"` // access | refresh,旧版无此字段视为 access
|
||||||
|
FamilyID string `json:"fid,omitempty"` // refresh 专用:会话族 ID
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateJWTTokenForAdmin 生成管理员JWT令牌
|
// generateJWTTokenForAdmin 生成管理员 access JWT 令牌
|
||||||
// - 包含管理员用户名信息和密码哈希
|
// - 包含管理员用户名信息和密码哈希
|
||||||
// - 设置过期时间
|
// - 设置过期时间
|
||||||
// - 使用HMAC-SHA256签名
|
// - 使用HMAC-SHA256签名
|
||||||
func generateJWTTokenForAdmin(username, passwordHash string, adminUUID string, role int) (string, error) {
|
func generateJWTTokenForAdmin(username, passwordHash string, adminUUID string, role int) (string, error) {
|
||||||
// 生成密码哈希摘要(使用SHA256)
|
|
||||||
// 注意:传入的 passwordHash 已经是数据库存的 Hash,这里我们再次 Hash 还是直接用?
|
|
||||||
// atomicLibrary 的实现是: utils.GenerateSHA256Hash(adminUser.Password)
|
|
||||||
// 这里我们直接用数据库里的 Hash 值作为 Token 的一部分即可,或者对它再 Hash 一次。
|
|
||||||
// 为了与 validateAdminPasswordHash 对应,我们需要知道验证时怎么比对。
|
|
||||||
// validateAdminPasswordHash: currentPasswordHash := utils.GenerateSHA256Hash(adminPassword.Value)
|
|
||||||
// 所以这里也应该对数据库里的值进行 Hash。
|
|
||||||
passwordHashDigest := utils.GenerateSHA256Hash(passwordHash)
|
passwordHashDigest := utils.GenerateSHA256Hash(passwordHash)
|
||||||
|
|
||||||
claims := JWTClaims{
|
claims := JWTClaims{
|
||||||
Username: username,
|
Username: username,
|
||||||
UUID: adminUUID,
|
UUID: adminUUID,
|
||||||
Role: role, // 用户真实角色
|
Role: role,
|
||||||
PasswordHash: passwordHashDigest, // 包含密码哈希摘要
|
PasswordHash: passwordHashDigest,
|
||||||
|
TokenType: TokenTypeAccess,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(services.GetSettingsService().GetJWTExpire()) * time.Hour)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(services.GetSettingsService().GetJWTExpire()) * time.Hour)),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
@@ -380,6 +502,32 @@ func generateJWTTokenForAdmin(username, passwordHash string, adminUUID string, r
|
|||||||
return token.SignedString(getJWTSecret())
|
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令牌
|
// parseJWTToken 解析并验证JWT令牌
|
||||||
// - 验证签名有效性
|
// - 验证签名有效性
|
||||||
// - 检查过期时间
|
// - 检查过期时间
|
||||||
@@ -403,20 +551,16 @@ func parseJWTToken(tokenString string) (*JWTClaims, error) {
|
|||||||
return nil, fmt.Errorf("invalid token")
|
return nil, fmt.Errorf("invalid token")
|
||||||
}
|
}
|
||||||
|
|
||||||
// getJWTCookie 获取JWT cookie的通用函数,支持从Cookie或Authorization Header中获取
|
// getJWTCookie 从 Authorization Bearer 头中读取 access token
|
||||||
|
// 注意:函数名保留以兼容历史调用,已不再读取 Cookie
|
||||||
func getJWTCookie(c *gin.Context) (string, error) {
|
func getJWTCookie(c *gin.Context) (string, error) {
|
||||||
cookie, err := c.Cookie("admin_session")
|
|
||||||
if err == nil && cookie != "" {
|
|
||||||
return cookie, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果Cookie中没有,尝试从Authorization Header中获取 (兼容前端在非HTTPS环境下无法设置Secure Cookie的情况)
|
|
||||||
authHeader := c.GetHeader("Authorization")
|
authHeader := c.GetHeader("Authorization")
|
||||||
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
|
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
|
||||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
|
if token != "" {
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return "", fmt.Errorf("未找到会话信息")
|
return "", fmt.Errorf("未找到会话信息")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,10 +739,10 @@ func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
|
|||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentAdminUserWithRefresh 获取当前登录的管理员用户信息并自动刷新令牌
|
// GetCurrentAdminUserWithRefresh 获取当前登录的管理员用户信息
|
||||||
// - 从JWT令牌中提取用户信息
|
// - 仅校验 access token 是否有效(不再做滑动续期)
|
||||||
// - 自动刷新接近过期的令牌(剩余时间少于6小时时刷新)
|
// - 续期统一由前端调用 /refresh-token 完成(OAuth2 风格)
|
||||||
// - 返回用户ID、用户名、角色和是否刷新了令牌
|
// - 第二个返回值保留为 false 以兼容历史调用方
|
||||||
func GetCurrentAdminUserWithRefresh(c *gin.Context) (*JWTClaims, bool, error) {
|
func GetCurrentAdminUserWithRefresh(c *gin.Context) (*JWTClaims, bool, error) {
|
||||||
cookie, err := getJWTCookie(c)
|
cookie, err := getJWTCookie(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -610,60 +754,16 @@ func GetCurrentAdminUserWithRefresh(c *gin.Context) (*JWTClaims, bool, error) {
|
|||||||
return nil, false, fmt.Errorf("无效的会话信息")
|
return nil, false, fmt.Errorf("无效的会话信息")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证密码哈希
|
// access token 必须是 access 类型
|
||||||
|
if claims.TokenType != "" && claims.TokenType != TokenTypeAccess {
|
||||||
|
return nil, false, fmt.Errorf("令牌类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
if !validateAdminPasswordHash(claims, c) {
|
if !validateAdminPasswordHash(claims, c) {
|
||||||
return nil, false, fmt.Errorf("会话已失效,请重新登录")
|
return nil, false, fmt.Errorf("会话已失效,请重新登录")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要刷新令牌
|
return claims, false, nil
|
||||||
refreshed := false
|
|
||||||
|
|
||||||
// 动态获取刷新阈值:默认剩余时间少于6小时刷新
|
|
||||||
refreshThresholdHours := services.GetSettingsService().GetJWTRefresh()
|
|
||||||
if refreshThresholdHours <= 0 {
|
|
||||||
refreshThresholdHours = 6 // 默认值
|
|
||||||
}
|
|
||||||
refreshThreshold := time.Duration(refreshThresholdHours) * time.Hour
|
|
||||||
|
|
||||||
// 动态获取JWT总有效期
|
|
||||||
expireHours := services.GetSettingsService().GetJWTExpire()
|
|
||||||
if expireHours <= 0 {
|
|
||||||
expireHours = 24 // 默认值
|
|
||||||
}
|
|
||||||
|
|
||||||
// 动态获取Cookie配置(用于更新Cookie过期时间)
|
|
||||||
secure, sameSite, domain, maxAge := services.GetSettingsService().GetCookieConfig()
|
|
||||||
|
|
||||||
// 1. 默认情况下,每次请求都更新Cookie的过期时间(滑动过期)
|
|
||||||
tokenToSet := cookie
|
|
||||||
shouldUpdateCookie := true
|
|
||||||
|
|
||||||
// 2. 检查是否需要刷新JWT令牌(生成新的Token)
|
|
||||||
if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
|
|
||||||
// 获取当前的 PasswordHash
|
|
||||||
db, _ := database.GetDB()
|
|
||||||
var adminUser models.User
|
|
||||||
db.Where("uuid = ? AND role = ?", claims.UUID, claims.Role).First(&adminUser)
|
|
||||||
|
|
||||||
// 使用新的有效期生成令牌
|
|
||||||
newToken, err := generateJWTTokenForAdmin(claims.Username, adminUser.Password, claims.UUID, adminUser.Role)
|
|
||||||
if err == nil {
|
|
||||||
tokenToSet = newToken
|
|
||||||
refreshed = true
|
|
||||||
|
|
||||||
// 更新当前claims的过期时间
|
|
||||||
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Duration(expireHours) * time.Hour))
|
|
||||||
claims.IssuedAt = jwt.NewNumericDate(time.Now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 执行Cookie更新
|
|
||||||
if shouldUpdateCookie {
|
|
||||||
cookieObj := utils.CreateSecureCookie("admin_session", tokenToSet, maxAge, domain, secure, sameSite)
|
|
||||||
c.SetCookie(cookieObj.Name, cookieObj.Value, cookieObj.MaxAge, cookieObj.Path, cookieObj.Domain, cookieObj.Secure, cookieObj.HttpOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
return claims, refreshed, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminAuthRequired 管理员认证拦截中间件 (Gin Middleware)
|
// AdminAuthRequired 管理员认证拦截中间件 (Gin Middleware)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func AutoMigrate() error {
|
|||||||
&models.API{},
|
&models.API{},
|
||||||
&models.Variable{},
|
&models.Variable{},
|
||||||
&models.Function{},
|
&models.Function{},
|
||||||
|
&models.RefreshToken{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logrus.WithError(err).Error("AutoMigrate 执行失败")
|
logrus.WithError(err).Error("AutoMigrate 执行失败")
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -61,20 +61,25 @@ func SeedDefaultSettings() error {
|
|||||||
Value: jwtSecret,
|
Value: jwtSecret,
|
||||||
Description: "JWT签名密钥",
|
Description: "JWT签名密钥",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "jwt_refresh",
|
|
||||||
Value: "6",
|
|
||||||
Description: "JWT令牌刷新阈值(小时)",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "jwt_expire",
|
Name: "jwt_expire",
|
||||||
Value: "24",
|
Value: "2",
|
||||||
Description: "JWT令牌有效期(小时)",
|
Description: "accessToken 有效期(小时),建议 0.5~2 小时",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "session_timeout",
|
Name: "refresh_token_expire_days",
|
||||||
Value: "3600",
|
Value: "7",
|
||||||
Description: "会话超时时间(秒),默认1小时",
|
Description: "refreshToken 滑动有效期(天),每次刷新可重新计算",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "session_absolute_expire_days",
|
||||||
|
Value: "30",
|
||||||
|
Description: "会话绝对过期上限(天),超过必须重新登录,refreshToken 滑动续期不能突破此上限",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "refresh_advance_seconds",
|
||||||
|
Value: "60",
|
||||||
|
Description: "accessToken 提前多少秒触发刷新(前端读取)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "max_upload_size",
|
Name: "max_upload_size",
|
||||||
@@ -110,6 +115,11 @@ func SeedDefaultSettings() error {
|
|||||||
Value: "10000",
|
Value: "10000",
|
||||||
Description: "操作日志保留条数(0表示不按数量清理)",
|
Description: "操作日志保留条数(0表示不按数量清理)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "refresh_token_cleanup_days",
|
||||||
|
Value: "7",
|
||||||
|
Description: "刷新令牌过期后保留天数(0表示不自动清理)",
|
||||||
|
},
|
||||||
}...)
|
}...)
|
||||||
|
|
||||||
// ===== Cookie相关默认项 =====
|
// ===== Cookie相关默认项 =====
|
||||||
@@ -324,11 +334,6 @@ func SeedDefaultSettings() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除已废弃的旧设置项,管理员登录入口改由门户导航控制
|
|
||||||
if err := db.Where("name = ?", "hide_login_entrance").Delete(&models.Settings{}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Info("系统设置初始化完成")
|
logrus.Info("系统设置初始化完成")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
29
models/refresh_token.go
Normal file
29
models/refresh_token.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// RefreshToken 刷新令牌持久化记录
|
||||||
|
// - 一次一换:每次刷新生成新 jti,并把旧记录标记 Revoked
|
||||||
|
// - 同一登录会话共享 FamilyID,便于整体撤销
|
||||||
|
// - 重用检测:一旦某条已 Revoked 的 token 被再次提交,整个 family 全部失效
|
||||||
|
type RefreshToken struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
JTI string `gorm:"uniqueIndex;size:64;not null;comment:JWT ID" json:"jti"`
|
||||||
|
FamilyID string `gorm:"index;size:64;not null;comment:同一登录会话的 token 族" json:"family_id"`
|
||||||
|
UserUUID string `gorm:"index;size:36;not null;comment:用户UUID" json:"user_uuid"`
|
||||||
|
UserType string `gorm:"size:16;not null;comment:用户类型 admin/user" json:"user_type"`
|
||||||
|
IssuedAt time.Time `gorm:"not null;comment:签发时间" json:"issued_at"`
|
||||||
|
ExpiresAt time.Time `gorm:"not null;comment:过期时间" json:"expires_at"`
|
||||||
|
AbsoluteExpiresAt time.Time `gorm:"not null;comment:会话绝对过期时间" json:"absolute_expires_at"`
|
||||||
|
Revoked bool `gorm:"not null;default:false;comment:是否已撤销" json:"revoked"`
|
||||||
|
ReplacedBy string `gorm:"size:64;comment:被哪个新 jti 替换" json:"replaced_by"`
|
||||||
|
UserAgent string `gorm:"size:255;comment:登录设备 UA" json:"user_agent"`
|
||||||
|
IP string `gorm:"size:64;comment:登录 IP" json:"ip"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (RefreshToken) TableName() string {
|
||||||
|
return "refresh_tokens"
|
||||||
|
}
|
||||||
@@ -47,6 +47,16 @@ func cleanupLogs() {
|
|||||||
logrus.WithError(err).Error("清理操作日志失败")
|
logrus.WithError(err).Error("清理操作日志失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理刷新令牌(已过期的记录,包括已撤销的旧记录)
|
||||||
|
refreshTokenDays := getSettingInt("refresh_token_cleanup_days", 7)
|
||||||
|
if refreshTokenDays > 0 {
|
||||||
|
if err := GetRefreshTokenService().CleanupExpired(refreshTokenDays); err != nil {
|
||||||
|
logrus.WithError(err).Error("清理刷新令牌失败")
|
||||||
|
} else {
|
||||||
|
logrus.Debugf("清理刷新令牌完成 (保留 %d 天)", refreshTokenDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logrus.Debug("日志清理任务执行完成")
|
logrus.Debug("日志清理任务执行完成")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
121
services/refresh_token.go
Normal file
121
services/refresh_token.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"NetworkAuth/database"
|
||||||
|
"NetworkAuth/models"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RefreshTokenService 提供 refreshToken 的持久化、轮换、撤销等业务能力
|
||||||
|
type RefreshTokenService struct{}
|
||||||
|
|
||||||
|
var refreshTokenService = &RefreshTokenService{}
|
||||||
|
|
||||||
|
// GetRefreshTokenService 单例获取
|
||||||
|
func GetRefreshTokenService() *RefreshTokenService {
|
||||||
|
return refreshTokenService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJTI 生成新的 jti
|
||||||
|
func (s *RefreshTokenService) NewJTI() string {
|
||||||
|
return uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFamilyID 生成新的 family_id(每次登录都新建)
|
||||||
|
func (s *RefreshTokenService) NewFamilyID() string {
|
||||||
|
return uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 持久化一条 refreshToken 记录
|
||||||
|
// - 登录场景:传入新的 familyID + absolute(now + 绝对过期天数)
|
||||||
|
// - 刷新场景:复用旧 familyID 与旧 absolute,保证滑动续期不能突破上限
|
||||||
|
func (s *RefreshTokenService) Create(jti, familyID, userUUID, userType string,
|
||||||
|
expiresAt, absoluteExpiresAt time.Time, ua, ip string) error {
|
||||||
|
db, err := database.GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rec := models.RefreshToken{
|
||||||
|
JTI: jti,
|
||||||
|
FamilyID: familyID,
|
||||||
|
UserUUID: userUUID,
|
||||||
|
UserType: userType,
|
||||||
|
IssuedAt: time.Now(),
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
AbsoluteExpiresAt: absoluteExpiresAt,
|
||||||
|
UserAgent: ua,
|
||||||
|
IP: ip,
|
||||||
|
}
|
||||||
|
return db.Create(&rec).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByJTI 根据 jti 查询
|
||||||
|
func (s *RefreshTokenService) FindByJTI(jti string) (*models.RefreshToken, error) {
|
||||||
|
db, err := database.GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var rec models.RefreshToken
|
||||||
|
if err := db.Where("jti = ?", jti).First(&rec).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &rec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeFamily 撤销整个 family 下所有未撤销的 refreshToken(用于重用检测/登出)
|
||||||
|
func (s *RefreshTokenService) RevokeFamily(familyID string) error {
|
||||||
|
db, err := database.GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return db.Model(&models.RefreshToken{}).
|
||||||
|
Where("family_id = ? AND revoked = ?", familyID, false).
|
||||||
|
Update("revoked", true).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeByJTI 撤销单条 refreshToken(一般在轮换时使用 Rotate)
|
||||||
|
func (s *RefreshTokenService) RevokeByJTI(jti string) error {
|
||||||
|
db, err := database.GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return db.Model(&models.RefreshToken{}).
|
||||||
|
Where("jti = ?", jti).
|
||||||
|
Update("revoked", true).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate 标记旧 jti 为已撤销,并记录被替换为新 jti
|
||||||
|
func (s *RefreshTokenService) Rotate(oldJTI, newJTI string) error {
|
||||||
|
db, err := database.GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return db.Model(&models.RefreshToken{}).
|
||||||
|
Where("jti = ?", oldJTI).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"revoked": true,
|
||||||
|
"replaced_by": newJTI,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupExpired 清理过期且过期时间早于 retentionDays 天前的记录
|
||||||
|
func (s *RefreshTokenService) CleanupExpired(retentionDays int) error {
|
||||||
|
db, err := database.GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cutoff := time.Now().AddDate(0, 0, -retentionDays)
|
||||||
|
return db.Where("expires_at < ?", cutoff).Delete(&models.RefreshToken{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrRefreshNotFound refreshToken 不存在
|
||||||
|
var ErrRefreshNotFound = errors.New("refresh token not found")
|
||||||
|
|
||||||
|
// IsNotFound 判断是否为记录未找到错误
|
||||||
|
func IsNotFound(err error) bool {
|
||||||
|
return errors.Is(err, gorm.ErrRecordNotFound)
|
||||||
|
}
|
||||||
@@ -144,11 +144,6 @@ func (s *SettingsService) RefreshCache() {
|
|||||||
s.loadAllSettings()
|
s.loadAllSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSessionTimeout 获取会话超时时间(秒)
|
|
||||||
func (s *SettingsService) GetSessionTimeout() int {
|
|
||||||
return s.GetInt("session_timeout", 3600) // 默认1小时
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsMaintenanceMode 检查是否开启维护模式
|
// IsMaintenanceMode 检查是否开启维护模式
|
||||||
func (s *SettingsService) IsMaintenanceMode() bool {
|
func (s *SettingsService) IsMaintenanceMode() bool {
|
||||||
return s.GetBool("maintenance_mode", false)
|
return s.GetBool("maintenance_mode", false)
|
||||||
@@ -164,14 +159,36 @@ func (s *SettingsService) GetEncryptionKey() string {
|
|||||||
return s.GetString("encryption_key", "")
|
return s.GetString("encryption_key", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetJWTRefresh 获取JWT刷新时间(小时)
|
// GetJWTRefresh 已废弃,请使用 GetRefreshTokenExpireDays
|
||||||
func (s *SettingsService) GetJWTRefresh() int {
|
func (s *SettingsService) GetJWTRefresh() int {
|
||||||
return s.GetInt("jwt_refresh", 6)
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetJWTExpire 获取JWT有效期(小时)
|
// GetJWTExpire 获取 accessToken 有效期(小时)
|
||||||
func (s *SettingsService) GetJWTExpire() int {
|
func (s *SettingsService) GetJWTExpire() int {
|
||||||
return s.GetInt("jwt_expire", 24)
|
v := s.GetInt("jwt_expire", 2)
|
||||||
|
if v <= 0 {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRefreshTokenExpireDays 获取 refreshToken 有效期(天)
|
||||||
|
func (s *SettingsService) GetRefreshTokenExpireDays() int {
|
||||||
|
v := s.GetInt("refresh_token_expire_days", 7)
|
||||||
|
if v <= 0 {
|
||||||
|
return 7
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSessionAbsoluteExpireDays 获取会话绝对过期上限(天)
|
||||||
|
func (s *SettingsService) GetSessionAbsoluteExpireDays() int {
|
||||||
|
v := s.GetInt("session_absolute_expire_days", 30)
|
||||||
|
if v <= 0 {
|
||||||
|
return 30
|
||||||
|
}
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCookieConfig 获取Cookie配置
|
// GetCookieConfig 获取Cookie配置
|
||||||
|
|||||||
Reference in New Issue
Block a user