开源前加固认证与部署契约:加密随机 JWT、改密事务吊销、登录 CSRF,并禁止生产回落 localhost。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-23 02:07:34 +08:00
parent 6f054a903c
commit 3b550f2124
24 changed files with 372 additions and 355 deletions

View File

@@ -47,7 +47,8 @@ var (
// ConfigureCookieNames 生产环境(HTTPS)启用 __Host- 前缀:
// 浏览器强制 Secure、Path=/、禁止 Domain,进一步收窄 cookie 作用面。
// 必须在路由注册前调用一次;前端按 NODE_ENV=production 同步启用。
// 必须在路由注册前调用一次;前端以 DEV_MODE 为准(与后端 !DevMode 对齐),
// 未设置 DEV_MODE 时回退 NODE_ENV=production。
func ConfigureCookieNames(useHostPrefix bool) {
if !useHostPrefix {
return
@@ -84,43 +85,51 @@ func NewAuthService(db *gorm.DB, jwtSecret string) *AuthService {
var dummyPasswordHash, _ = bcrypt.GenerateFromPassword(
[]byte("j13-dummy-password-for-constant-timing"), bcrypt.DefaultCost)
// Register 用户注册
// Register 用户注册(事务 + 咨询锁,保证空库至多一名 owner)
func (s *AuthService) Register(username, email, password string) (*model.User, error) {
if err := NewOperations(s.db, nil).Filter("username", username, 0); err != nil {
return nil, err
}
// 检查用户名是否已存在
var count int64
s.db.Model(&model.User{}).Where("username = ?", username).Count(&count)
if count > 0 {
return nil, errors.New("用户名已被使用")
}
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
var n int64
if err := s.db.Model(&model.User{}).Count(&n).Error; err != nil {
var user model.User
err = s.db.Transaction(func(tx *gorm.DB) error {
// 与站长角色迁移共用锁空间,避免并发首注抢多 owner
if err := tx.Exec("SELECT pg_advisory_xact_lock(?)", int64(0x6a31336f776e6572)).Error; err != nil {
return err
}
var count int64
if err := tx.Model(&model.User{}).Where("username = ?", username).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("用户名已被使用")
}
var n int64
if err := tx.Model(&model.User{}).Count(&n).Error; err != nil {
return err
}
role := model.RoleUser
if n == 0 {
role = model.RoleOwner
}
user = model.User{
Username: username,
Email: email,
Password: string(hashed),
Nickname: username,
Role: role,
}
return tx.Create(&user).Error
})
if err != nil {
return nil, err
}
role := model.RoleUser
if n == 0 {
role = model.RoleOwner
}
user := &model.User{
Username: username,
Email: email,
Password: string(hashed),
Nickname: username,
Role: role,
}
if err := s.db.Create(user).Error; err != nil {
return nil, err
}
return user, nil
return &user, nil
}
// Login 用户登录,返回 access token + refresh token + user
@@ -479,18 +488,22 @@ func (s *AuthService) RotateRefreshToken(oldToken, ip, ua string) (string, strin
}
// RevokeRefreshToken 撤销单个 refresh token(单设备登出时用)
func (s *AuthService) RevokeRefreshToken(token string) {
func (s *AuthService) RevokeRefreshToken(token string) error {
if token == "" {
return
return nil
}
s.db.Model(&model.RefreshToken{}).Where("token_hash = ?", hashRefreshToken(token)).
Updates(map[string]any{"revoked": true, "token_cipher": ""})
return s.db.Model(&model.RefreshToken{}).Where("token_hash = ?", hashRefreshToken(token)).
Updates(map[string]any{"revoked": true, "token_cipher": ""}).Error
}
// RevokeAllUserRefreshTokens 撤销用户所有 refresh token(改密码/封禁/盗用处置时用)
func (s *AuthService) RevokeAllUserRefreshTokens(userID uint) {
s.db.Model(&model.RefreshToken{}).Where("user_id = ? AND revoked = ?", userID, false).
Updates(map[string]any{"revoked": true, "token_cipher": ""})
func (s *AuthService) RevokeAllUserRefreshTokens(userID uint) error {
return revokeAllUserRefreshTokensTx(s.db, userID)
}
func revokeAllUserRefreshTokensTx(db *gorm.DB, userID uint) error {
return db.Model(&model.RefreshToken{}).Where("user_id = ? AND revoked = ?", userID, false).
Updates(map[string]any{"revoked": true, "token_cipher": ""}).Error
}
// PurgeExpiredRefreshTokens 物理清理过期或吊销超过保留期的行
@@ -518,18 +531,18 @@ func StartRefreshTokenCleanup(db *gorm.DB) {
}()
}
// IncrementTokenVersion 递增用户 token 版本,使所有已有 JWT 失效
// 用于:改密码、封禁用户、管理员强制下线
// IncrementTokenVersion 递增用户 token 版本并吊销全部 refresh(单事务)
func (s *AuthService) IncrementTokenVersion(userID uint) error {
result := s.db.Model(&model.User{}).Where("id = ?", userID).UpdateColumn("token_version", gorm.Expr("token_version + 1"))
if result.Error != nil {
return result.Error
}
s.RevokeAllUserRefreshTokens(userID)
return nil
return s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&model.User{}).Where("id = ?", userID).
UpdateColumn("token_version", gorm.Expr("token_version + 1")).Error; err != nil {
return err
}
return revokeAllUserRefreshTokensTx(tx, userID)
})
}
// ChangePassword 修改密码:校验旧密码,更新新密码,递增 token_version 使旧 token 失效
// ChangePassword 修改密码:校验旧密码,更新新密码,事务内使旧会话全部失效
func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword string) error {
var user model.User
if err := s.db.First(&user, userID).Error; err != nil {
@@ -545,15 +558,20 @@ func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword strin
if err != nil {
return err
}
result := s.db.Model(&model.User{}).Where("id = ?", userID).Update("password", string(hashed))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("密码更新失败")
}
// 递增 token_version,使所有旧 JWT 和 refresh token 失效
return s.IncrementTokenVersion(userID)
return s.db.Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.User{}).Where("id = ?", userID).Update("password", string(hashed))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("密码更新失败")
}
if err := tx.Model(&model.User{}).Where("id = ?", userID).
UpdateColumn("token_version", gorm.Expr("token_version + 1")).Error; err != nil {
return err
}
return revokeAllUserRefreshTokensTx(tx, userID)
})
}
// UpdateProfile 更新昵称/邮箱/签名。头像只能通过上传接口(/api/upload/avatar)