fix(user): revoke all sessions and access tokens on password change

- Store user password hash in session during login

- Validate password hash compatibility on requests to prevent session reuse

- Revoke all user access tokens and clear session on ChangePassword
This commit is contained in:
ryan
2026-06-13 10:27:28 +08:00
parent eb999eba09
commit 26e12594a2
5 changed files with 167 additions and 21 deletions
+1
View File
@@ -17,6 +17,7 @@ const (
TokenAuthKey = "token_auth" // 标记当前请求是否通过 Access Token 鉴权
TokenAdminKey = "token_admin" // Access Token 本身是否具有管理员权限
SessionTokenKey = "oauth_session_token" //nolint:gosec // false positive: this is a session key, not hardcoded credentials
PasswordHashKey = "password_hash"
)
// OAuth State 缓存 Key 格式与过期时间
+25 -19
View File
@@ -13,6 +13,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/otel_trace"
"github.com/Rain-kl/Wavelet/internal/util"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
@@ -41,39 +42,44 @@ func GetUserFromRequest(c *gin.Context) (*model.User, error) {
}
var user model.User
var authenticated bool
var tokenAuth bool
var tokenAdmin bool
// 优先使用 Access Token 鉴权
if tokenStr != "" {
tokenHash := model.HashToken(tokenStr)
var tokenRecord model.AccessToken
if err := db.DB(ctx).Where("token_hash = ?", tokenHash).First(&tokenRecord).Error; err == nil {
if err := db.DB(ctx).Where("id = ? AND is_active = ?", tokenRecord.UserID, true).First(&user).Error; err == nil {
authenticated = true
tokenAuth = true
tokenAdmin = tokenRecord.IsAdmin
util.SetToContext(c, TokenAuthKey, true)
util.SetToContext(c, TokenAdminKey, tokenRecord.IsAdmin)
return &user, nil
}
}
}
if !authenticated {
// load user from session
userID := GetUserIDFromContext(c)
if userID <= 0 {
return nil, errors.New("unauthorized")
}
// 降级使用 Session 鉴权
userID := GetUserIDFromContext(c)
if userID <= 0 {
return nil, errors.New("unauthorized")
}
// load user from db to make sure is active
tx := db.DB(ctx).Where("id = ? AND is_active = ?", userID, true).First(&user)
if tx.Error != nil {
return nil, tx.Error
// load user from db to make sure is active
tx := db.DB(ctx).Where("id = ? AND is_active = ?", userID, true).First(&user)
if tx.Error != nil {
return nil, tx.Error
}
// 密码哈希校验:当用户存在本地密码时,要求 Session 中的密码哈希必须与当前数据库中一致
if user.Password != "" {
session := sessions.Default(c)
sessionHash, _ := session.Get(PasswordHashKey).(string)
if sessionHash != user.Password {
return nil, errors.New("session expired due to password change")
}
}
// set keys in context
util.SetToContext(c, TokenAuthKey, tokenAuth)
util.SetToContext(c, TokenAdminKey, tokenAdmin)
// set keys in context for session auth
util.SetToContext(c, TokenAuthKey, false)
util.SetToContext(c, TokenAdminKey, false)
return &user, nil
}
+1
View File
@@ -197,6 +197,7 @@ func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) erro
session := sessions.Default(c)
session.Set(UserIDKey, user.ID)
session.Set(UserNameKey, user.Username)
session.Set(PasswordHashKey, user.Password)
// 根据系统配置动态设置 Session 过期时间
maxAge := config.Config.App.SessionAge