安全与可靠性: 修复配额并发、投票事务、密码授权与导出完整性

This commit is contained in:
tamakyi
2026-11-05 12:17:05 +08:00
parent 7636456d4e
commit 25aee35030
18 changed files with 1002 additions and 194 deletions
+43 -9
View File
@@ -5,6 +5,7 @@
package auth
import (
"fmt"
"os"
"time"
@@ -73,6 +74,20 @@ func ForgotPasswordAction(ctx context.Context, f form.ForgotPassword, cache cach
return
}
// SEC-05:签发新令牌前,先尝试删除该用户上一次的找回令牌。
// 用 userID → code 反查键实现:新令牌签发后旧令牌的反查键被覆盖,
// 旧 code 在 24h 内虽然物理上还在缓存里,但已经不在用户的"当前令牌"集合中,
// checkRecoverPasswordCode 会拒绝使用。
userCodeKey := "forgot-password-user-code:" + fmt.Sprint(user.ID)
if oldCodeItf, err := cache.Get(ctx.Request().Context(), userCodeKey); err == nil {
if oldCode, ok := oldCodeItf.(string); ok && oldCode != "" {
oldCodeKey := "forgot-password-recovery-code:" + oldCode
if err := cache.Delete(ctx.Request().Context(), oldCodeKey); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Warn("Failed to delete old recovery code")
}
}
}
code := randstr.String(64)
recoveryCodeCacheKey := "forgot-password-recovery-code:" + code
if err := cache.Set(ctx.Request().Context(), recoveryCodeCacheKey, user.ID, 24*time.Hour); err != nil {
@@ -82,6 +97,16 @@ func ForgotPasswordAction(ctx context.Context, f form.ForgotPassword, cache cach
return
}
// 记录该用户当前的找回令牌 code,供下次签发时撤销。
if err := cache.Set(ctx.Request().Context(), userCodeKey, code, 24*time.Hour); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to set user recovery code cache")
// 尽力回滚刚写入的令牌,避免孤儿
_ = cache.Delete(ctx.Request().Context(), recoveryCodeCacheKey)
ctx.SetInternalErrorFlash()
ctx.Redirect("/forgot-password")
return
}
if err := mail.SendPasswordRecoveryMail(user.Email, code); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to send password recovery mail")
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "auth.errEmailFailed"))
@@ -151,15 +176,10 @@ func RecoverPasswordAction(ctx context.Context, cache cache.Cache, f form.Recove
return
}
code := ctx.Query("code")
recoveryCodeCacheKey := "forgot-password-recovery-code:" + code
if err := cache.Delete(ctx.Request().Context(), recoveryCodeCacheKey); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to delete password recovery code cache")
ctx.SetInternalErrorFlash()
ctx.Redirect("/login")
return
}
// SEC-05:先更新密码,成功后再删除令牌。
// 旧实现先删令牌再改密码:若密码更新失败,用户必须重新申请。
// 现在密码更新成功才删令牌;即使删令牌失败,密码已改,
// 旧令牌也不再有用(重新登录需要新密码)。
if err := db.Users.UpdatePassword(ctx.Request().Context(), user.ID, f.NewPassword); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to update user password")
ctx.SetInternalErrorFlash()
@@ -167,6 +187,20 @@ func RecoverPasswordAction(ctx context.Context, cache cache.Cache, f form.Recove
return
}
code := ctx.Query("code")
recoveryCodeCacheKey := "forgot-password-recovery-code:" + code
if err := cache.Delete(ctx.Request().Context(), recoveryCodeCacheKey); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to delete password recovery code cache")
// 令牌删除失败不阻断流程:密码已改,旧令牌已经无法用于登录。
// 但记录日志便于排查缓存异常。
}
// 同时清理 user → code 反查键,避免下次申请时尝试删除已不存在的旧 code。
userCodeKey := "forgot-password-user-code:" + fmt.Sprint(user.ID)
if err := cache.Delete(ctx.Request().Context(), userCodeKey); err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Warn("Failed to delete user recovery code mapping")
}
ctx.SetSuccessFlash(i18n.T(ctx.Lang(), "auth.errPasswordChanged"))
ctx.Redirect("/login")
}