← [ BACK_TO_NOTES ]

BodySense 认证完整流程

BodySense 项目的完整认证流程:注册、登录、Token 刷新、注销、密码重置、邮箱验证的全链路设计。

#concept #growing #backend

[!info] related notes

BodySense 认证完整流程

认证流程总览

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   注册       │ ──► │   登录       │ ──► │   使用       │
│             │     │             │     │             │
│ 1. 验证邮箱  │     │ 1. 验证密码  │     │ 1. 带 Token  │
│ 2. 哈希密码  │     │ 2. 生成双Token│     │ 2. 中间件验证│
│ 3. 存储用户  │     │ 3. 返回响应  │     │ 3. 注入上下文│
└─────────────┘     └─────────────┘     └─────────────┘
                           │
                           ▼
                    ┌─────────────┐     ┌─────────────┐
                    │   刷新       │ ──► │   注销       │
                    │             │     │             │
                    │ 1. 验证 RT  │     │ 1. 加黑名单  │
                    │ 2. 生成新AT │     │ 2. 清除 RT   │
                    │ 3. 返回响应  │     │             │
                    └─────────────┘     └─────────────┘

1. 注册流程

完整代码

// handler/auth.go
func (h *AuthHandler) Register(c *gin.Context) {
    var req RegisterRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": "invalid request"})
        return
    }

    // 1. 检查邮箱是否已注册
    existing, _ := h.userRepo.GetByEmail(c.Request.Context(), req.Email)
    if existing != nil {
        c.JSON(409, gin.H{"error": "email already registered"})
        return
    }

    // 2. 密码强度验证
    if err := validatePassword(req.Password); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }

    // 3. 当前示例仍以 bcrypt verifier 表示;新系统优先考虑 Argon2id。
    // cost 不能写成永久“推荐值”,应按当前硬件/并发基准校准。
    hashedPassword, err := bcrypt.GenerateFromPassword(
        []byte(req.Password),
        12,
    )
    if err != nil {
        c.JSON(500, gin.H{"error": "internal error"})
        return
    }

    // 4. 创建用户
    user := &User{
        ID:           uuid.New(),
        Email:        req.Email,
        PasswordHash: string(hashedPassword),
        Name:         req.Name,
    }
    if err := h.userRepo.Create(c.Request.Context(), user); err != nil {
        c.JSON(500, gin.H{"error": "internal error"})
        return
    }

    // 5. 生成 Token 对
    tokens, err := h.generateTokenPair(user)
    if err != nil {
        c.JSON(500, gin.H{"error": "internal error"})
        return
    }

    // 6. 存储 Refresh Token 到数据库
    if err := h.saveRefreshToken(user.ID, tokens.RefreshToken); err != nil {
        c.JSON(500, gin.H{"error": "internal error"})
        return
    }

    c.JSON(201, gin.H{
        "user":         user.ToResponse(),
        "access_token": tokens.AccessToken,
        "expires_in":   3600 * 24 * 7,  // 7 天
    })
}

密码策略:长度优先,不再强制字符组合

本文旧版采用“8+ 字符 + 大小写 + 数字”的 composition rule。按现行 NIST SP 800-63B-4,BodySense 的目标策略应改为:

  • 单因素密码登录:至少 15 个 Unicode code point;
  • 密码仅作为 MFA 中一个因素:可允许至少 8 个字符;
  • 支持的最大长度至少 64 个字符,产品可设置更高的合理上限;
  • 接受空格和常见 Unicode;
  • 不强制“必须大写 + 小写 + 数字 + 符号”;
  • 新建/改密时检查常见、预期或已泄漏密码 blocklist;
  • 不做无原因的周期性强制改密;有 compromise evidence 时才强制变更;
  • 在线登录通过 rate limiting / lockout policy 抑制猜测。

如果继续使用 bcrypt,要额外注意它通常只处理前 72 bytes。这与“至少支持 64 个 Unicode 字符”的现代可用性目标可能冲突,因为一个 Unicode 字符可能占多个 UTF-8 bytes。

因此 bcrypt 的 72-byte 边界应该视为:

legacy implementation constraint,而不是密码策略优势。

新实现更适合迁移到 Argon2id,避免静默截断,也获得 memory-hard 成本模型。

当前 bcrypt 实现与迁移目标

本文代码示例使用 bcrypt,这是一个成熟且仍可安全维护的 password hashing scheme,但不应再写成“bcrypt 是当前统一推荐答案”。

方案当前定位
SHA-256(password)❌ 快速 hash,不适合 password storage
bcrypt✅ 既有系统可继续维护;注意 72-byte 上限与非 memory-hard
PBKDF2-HMAC-SHA256✅ 合规/兼容场景可用,iteration 参数需持续升级
Argon2id✅ 新系统优先考虑的 memory-hard password hashing scheme

BodySense 的策略应是:

现有 bcrypt verifier
→ 正常 verify
→ 登录成功
→ 检查 verifier policy
→ 如需要则 Argon2id rehash
→ 原子替换 stored verifier

这样不需要一次性要求所有用户重置密码。

更多模型见 Password Storage & Verification。

2. 登录流程

BodySense 登录不应只理解成:

查用户
→ bcrypt compare
→ 错 5 次锁 15 分钟

更完整的安全链路应该是:

request
→ coarse source/IP rate limit
→ normalize account identifier
→ account/source risk throttle
→ lookup account without enumeration leak
→ verify stored password verifier
→ record success/failure signals
→ optional MFA / step-up
→ issue session/tokens

当前实现事实 vs 目标模型

当前项目知识中仍存在两类机制:

  1. Auth Redis fixed-window rate limit,例如 Login 10 / 5 minutes;
  2. 旧示例中的 account failure counter / temporary lockout。

这些都可以继续作为实现基础,但不能把:

5 failures
→ hard lock 15 minutes

当成完整认证防线。

原因:

  • distributed attacker 可以换 IP;
  • password spraying 每个账号只试少量次数;
  • 固定 lockout 可被攻击者用来 DoS 受害者;
  • credential stuffing 可能一次就用正确泄漏密码登录成功。

目标模型见 Authentication Throttling & Account Lockout。

推荐登录编排

伪代码:

func (h *AuthHandler) Login(c *gin.Context) {
    // 1. 先做粗粒度 source/IP 限流,挡明显洪水
    // 2. 规范化 email/account identifier
    // 3. 检查 account + source + cross-account risk state
    // 4. 查询账号;公共错误不要泄漏账号是否存在
    // 5. 使用 stored verifier 自带 scheme/parameters 验证 password
    // 6. failure: 记录 account/source/device 等信号,应用 progressive throttle
    // 7. success: 如 verifier 过旧,执行 rehash-on-login
    // 8. 如风险高或策略要求,完成 MFA/step-up
    // 9. 生成 Access/Refresh 或 Session
    // 10. 更新安全日志;清理“连续失败”状态,但保留历史风险指标
}

这里不再把业务层写死成:

bcrypt.CompareHashAndPassword(...)

更稳定的是:

PasswordVerifier.verify(storedVerifier, candidatePassword)

这样 stored verifier 可以从 bcrypt 渐进迁移到 Argon2id。

Account Enumeration

对不存在账号和错误密码:

public response
→ 尽量统一

例如:

invalid email or password

不要直接返回:

email not registered

同时注意 timing 也可能成为侧信道。

可以通过:

  • 统一主要代码路径;
  • bounded dummy verification;
  • 异步发送 forgot-password;
  • 不暴露精细 lockout 状态;

降低 enumeration。

Authentication Throttling

不要只维护一个:

login_failures:<user_id>

更完整的 risk dimensions 可能包括:

account
IP / subnet
device/browser
account × source
unique accounts touched by source
unique sources touching account
time window

BodySense 已有 Redis,可继续作为跨实例共享 throttling state。

Progressive Delay / Challenge

比固定 hard lock 更稳的一种方向:

few failures
→ normal/gentle delay

more failures
→ longer delay

suspicious pattern
→ CAPTCHA / MFA / WebAuthn step-up

extreme/confirmed compromise
→ authenticator disable / recovery flow

这减少 lockout DoS 风险。

登录成功后的 Counter

旧示例会:

success
→ clear all login failure state

更合理的是区分:

  • consecutive failure counter:成功后可清;
  • historical attack signal:不应立即全部抹掉;
  • known-device / risk state:独立保存;
  • security telemetry:保留用于检测 stuffing/spraying。

因此一个 Redis counter 不应承担完整 risk engine。

MFA

如果:

password valid
+
account requires MFA

不能直接签发最终高 assurance session。

应:

password valid
→ pending authentication state
→ MFA/WebAuthn challenge
→ MFA valid
→ final session/token

高风险登录也可以 adaptive step-up。

详见 Password 与 MFA 的安全边界。

3. Token 刷新流程

func (h *AuthHandler) RefreshToken(c *gin.Context) {
    var req RefreshRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": "invalid request"})
        return
    }

    // 1. 验证 Refresh Token
    claims, err := auth.ValidateRefreshToken(h.jwtConfig, req.RefreshToken)
    if err != nil {
        c.JSON(401, gin.H{"error": "invalid refresh token"})
        return
    }

    // 2. 检查 Refresh Token 是否在数据库中(未被注销)
    stored, err := h.refreshTokenRepo.GetByUserID(c.Request.Context(), claims.UserID)
    if err != nil || stored.Token != req.RefreshToken {
        // 可能是被盗用的旧 Token,注销该用户所有 Token
        h.refreshTokenRepo.DeleteByUserID(c.Request.Context(), claims.UserID)
        c.JSON(401, gin.H{"error": "invalid refresh token"})
        return
    }

    // 3. 检查 Refresh Token 是否过期
    if stored.ExpiresAt.Before(time.Now()) {
        h.refreshTokenRepo.Delete(c.Request.Context(), stored.ID)
        c.JSON(401, gin.H{"error": "refresh token expired"})
        return
    }

    // 4. 获取用户信息
    user, err := h.userRepo.GetByID(c.Request.Context(), claims.UserID)
    if err != nil || user == nil {
        c.JSON(401, gin.H{"error": "user not found"})
        return
    }

    // 5. 生成新的 Token 对
    tokens, err := h.generateTokenPair(user)
    if err != nil {
        c.JSON(500, gin.H{"error": "internal error"})
        return
    }

    // 6. 更新 Refresh Token(旋转)
    h.refreshTokenRepo.Update(c.Request.Context(), stored.ID, tokens.RefreshToken)

    c.JSON(200, gin.H{
        "access_token": tokens.AccessToken,
        "expires_in":   3600 * 24 * 7,
    })
}

Refresh Token 旋转

为什么要旋转:每次刷新都生成新的 Refresh Token,旧的立即失效。这样即使旧 Token 被盗,也只能用一次。

RT1 (旧) → 刷新 → RT2 (新) + AT
RT1 立即失效
RT2 → 刷新 → RT3 (新) + AT
RT2 立即失效

4. 注销与 Session 撤销

旧模型把:

raw Access Token
→ Redis blacklist

作为主要 Logout 机制。

按当前 BodySense 的 Redis Session Authority / Refresh rotation 心智模型,更稳定的职责是:

Logout
→ revoke current Session / Refresh family
→ clear client credential
→ old short-lived Access Token 进入有限 stale window

如果某条受保护 API 要求“立刻撤销”,则必须额外查询 Session authority / security version,而不是只靠本地 JWT 验签。

详见 Credential Change & Session Revocation。

当前设备 Logout

目标状态转换:

authenticated request
→ resolve session_id / grant_id
→ atomically revoke current session
→ revoke current refresh family
→ delete online Redis authority state
→ client clears Cookie / local access state

服务端 revoke 才是真正安全边界。

仅:

frontend clear token

不能阻止已经复制 credential 的攻击者继续使用。

Logout All Devices

退出所有设备:

user_id
→ enumerate active sessions
→ revoke all
→ revoke all refresh families
→ optionally increment security_version

Redis 中的 user_sessions:<uid> 可以作为 secondary index,但最终合法性仍以具体 session:<sid> authority 为准。

Access Token stale window

如果普通 API 只本地验证短期 JWT:

T0 revoke Session
→ already-issued Access Token
→ may remain valid until exp

这是架构 trade-off,不应假装“Logout 后所有 JWT 瞬间失效”。

如果产品要求即时失效,可选:

  • 每请求 Session authority check;
  • short Access Token TTL;
  • security_version / credential version online check;
  • denylist/revocation cache。

5. Password Reset / Account Recovery

BodySense 的 reset flow 应按独立 recovery protocol 建模:

request reset
→ generic public response
→ issue high-entropy single-use token
→ send via trusted email channel
→ verify token
→ restricted recovery state
→ validate new password policy
→ write new password verifier
→ revoke sessions / refresh families
→ notify user
→ require normal login

详见 Password Reset & Account Recovery。

5.1 Request Reset

请求阶段必须同时防:

  • account enumeration;
  • recovery email bombing;
  • source flooding。

公共响应保持统一,例如:

if the account exists, reset instructions will be sent

不要因为 email 不存在就走明显更快的早退路径。

限流至少包含:

  • account identifier;
  • source IP/device;
  • global abuse policy。

5.2 Reset Token

旧示例:

resetToken := uuid.New().String()
redis.Set("password_reset:"+resetToken, userID, 1*time.Hour)

不再作为推荐模型。

更稳:

CSPRNG raw token
→ email to user

SHA-256(raw token)
→ Redis / DB token_digest

服务端记录:

token_digest
user_id
purpose = PASSWORD_RESET
expires_at
used_at

这样 Redis/DB 泄漏不会直接拿到可使用的 reset bearer token。

完整边界见 Password Reset Token。

5.3 Atomic Consume

执行重置必须原子保证:

digest matches
AND not expired
AND unused
→ consume exactly once

不能:

GET token
→ update password
→ later DEL token

然后允许两个并发 reset 请求都成功。

Redis 可用 Lua / transaction;关系数据库可用条件 UPDATE/DELETE + transaction。

5.4 新 Password

Recovery flow 使用与正常注册/改密一致的:

新系统目标:

PasswordVerifier.hash(current policy)

而不是在 recovery handler 内写死:

bcrypt cost=12

5.5 Reset 后撤销

Recovery reset 属于高风险 credential event。

BodySense 应:

update password verifier
→ revoke all active sessions
→ revoke all refresh families
→ invalidate remembered-device trust if any
→ increment security_version when architecture uses it
→ security notification
→ require normal login

不要:

reset complete
→ automatically issue long-lived authenticated session

详见 Credential Change & Session Revocation。

5.6 MFA 边界

如果未来 BodySense 开启 MFA:

password reset
≠ MFA reset

Password reset 成功不能自动删除:

  • TOTP;
  • WebAuthn credential;
  • recovery codes;
  • device-bound factor。

MFA recovery 应走单独受控流程。

6. Email Verification 与 Email Change

BodySense 描述里一直包含“邮箱验证”,但旧正文没有正式流程。

应拆成:

Signup Verification

register
→ account pending/unverified
→ issue purpose=EMAIL_VERIFY token
→ send email
→ user clicks
→ atomically verify
→ set email_verified_at

Verification token 和 Password Reset Token 不能复用 purpose。

Email Change

已登录用户修改邮箱是高风险认证事件:

recent reauthentication / MFA
→ set pending_email
→ send verification to new email
→ verify new email
→ atomically promote pending_email
→ notify old email
→ review session/recovery state

不要:

PATCH /profile email=new@example.com
→ immediately replace recovery address

详见 Email Verification & Email Change Security。

7. Session / Grant 与 Access Token 生成

旧示例把:

Access Token = JWT 7 days
Refresh Token = JWT 30 days

写成固定答案。

按当前 BodySense 认证架构,更稳定的模型是:

Login / MFA success
→ create server-side Session / Grant
→ issue short-lived Access Token projection
→ issue/rotate Refresh credential bound to Session

长期登录 authority 在:

Redis Session Authority

而不是 Access Token 自身。

Session Record

BodySense 在线 Session 至少应该能表达:

session_id
user_id
created_at
expires_at
revoked_at
last_seen_at

auth_time
auth_methods
mfa_verified_at
assurance

device_label
client_type
user_agent_summary
coarse_ip / network context
risk_state

refresh_family_id / version

Redis 可以承载:

session:<sid>
→ hot live authority

PostgreSQL 是否额外保存长期 Session / Device audit metadata,取决于登录历史、设备管理、审计与 Redis 丢失后的恢复需求。

详见 Active Sessions & Device Management。

Access Token 是短期投影

Access Token 可以携带:

sub = user_id
sid = session_id
iat
exp
auth_time
coarse scopes/roles

如果需要 assurance claim,也必须明确它只是签发时快照。

Access Token claim
≠ 永久实时事实

Session 被 revoke、MFA 被移除、risk 升高后,Resource Server 是否即时知道,取决于是否查询在线 authority。

TTL 配置

不要把:

7 days
30 days

写成“短期/长期”的通用定义。

TTL 应配置化:

access_ttl
refresh/session_ttl
idle_timeout
reauth_window

并按:

  • threat model;
  • Session authority 是否每请求检查;
  • user friction;
  • sensitive action;
  • client type;

共同决定。

如果 BodySense 每个请求都会查 Redis Session authority,那么 Access Token stale-window 风险比纯 stateless JWT 小,但仍应保持合理短生命周期,减少凭证复制后的暴露面。

Refresh Credential

Refresh Token 更适合建模成:

high-entropy credential
→ server stores digest + family/session metadata
→ single-winner rotation
→ replay detection

而不是因为 Access Token 用 JWT,就机械让 Refresh Token 也使用自包含 JWT。

当前实现如果已经是 JWT Refresh,应把它视为 implementation fact,不要把它提升成架构必然。

Token/Session Rotation

以下事件应考虑轮换或重新签发:

  • login;
  • privilege/assurance elevation;
  • MFA completion;
  • reauthentication;
  • password change;
  • email/MFA security event。

避免同一个 Session identifier 在匿名态、弱认证态和高 assurance 态之间永久复用。

8. Active Sessions、Trusted Device 与 Reauthentication

Active Sessions

用户安全设置应能看到:

Current session
Chrome on Windows
last active ...
rough network/location ...

iPhone App
last active ...

底层必须基于真实 Session/Grant,而不是伪造一个不可验证的“物理设备 ID”。

支持:

  • revoke one session;
  • revoke all other sessions;
  • revoke all sessions after compromise。

详见 Active Sessions & Device Management。

Trusted Device / Remember MFA

未来如果 BodySense 提供:

Remember this device

不能简单存:

trusted=true
device_fingerprint=...

更稳模型:

MFA success
→ issue high-entropy remembered-device credential
→ server-side trust record / revocable representation
→ finite expiry
→ bind user + client context

Browser fingerprint、IP、User-Agent 只作为 risk signal,不作为认证 factor。

以下事件通常应失效 remembered trust:

  • password reset;
  • MFA reset/change;
  • account recovery;
  • suspicious login;
  • revoke device;
  • security_version change。

详见 Trusted Device & Remember MFA。

Reauthentication Freshness

高风险动作不能只检查:

Session exists

而要检查:

auth_time
mfa_verified_at
auth_methods
assurance
current risk

例如:

read own dashboard
→ existing session

change password
change email
disable MFA
export sensitive health data
create API key
→ recent strong reauthentication

具体 freshness window 应配置化并按动作风险决定。

详见 Authentication Assurance & Reauthentication Freshness。

Suspicious Login / Risk-Based Authentication

BodySense 可以把以下信号作为 risk context:

  • new browser/client;
  • new network / unusual ASN;
  • one source touching many accounts;
  • one account touched by many sources;
  • valid password + repeated MFA failure;
  • impossible-travel-like location jump;
  • recovery followed by sensitive action;
  • leaked-password signal。

这些都只是概率性 signal。

risk low
→ normal flow

risk medium
→ step-up MFA / WebAuthn
→ security notification

risk high
→ deny / restricted recovery
→ revoke suspicious session

不要:

IP changed
→ definite attacker

详见 Suspicious Login & Risk-Based Authentication。

9. Authentication Security Events / Login History

BodySense 目前只有零散:

last_login_at
Redis counters
Session state

这不足以支持完整调查和用户安全中心。

建议统一 security event vocabulary:

AUTHN_LOGIN_SUCCESS
AUTHN_LOGIN_FAILURE
AUTHN_MFA_SUCCESS
AUTHN_MFA_FAILURE
AUTHN_SESSION_REVOKED
AUTHN_PASSWORD_CHANGED
AUTHN_PASSWORD_RESET
AUTHN_EMAIL_CHANGED
AUTHN_MFA_CHANGED
AUTHN_RECOVERY_COMPLETED
AUTHN_TRUSTED_DEVICE_ADDED
AUTHN_RISK_STEP_UP

每条事件至少有:

event_id
event_type
occurred_at
user_id
session_id?
source/context metadata
auth methods
outcome
reason_code
correlation_id

不要记录:

  • plaintext password;
  • raw token;
  • TOTP secret;
  • recovery code;
  • raw reset token。

详见 Authentication Security Events & Login History。

用户可见 Login History

用户安全页可以展示 security events 的最小化投影:

time
browser/app
rough device
rough location/IP
success / revoked
current session

不要直接暴露内部:

risk_score
detection_rule
fraud feature

以免给攻击者反馈检测逻辑。

前端配合

Browser Credential Storage

旧示例把 Access Token 和 Refresh Token 都通过 Zustand persist 长期写入浏览器存储。

这不是推荐默认模型。

Browser Web 场景更稳的常见选择:

Refresh / Session credential
→ HttpOnly + Secure Cookie
→ JavaScript cannot directly read

Access Token
→ memory when architecture needs browser-side bearer token

或者更简单:

Browser
→ HttpOnly opaque Session Cookie
→ BFF / server-side Session

如果 BodySense Web 继续使用 Authorization Header + Access Token,也应避免把长期 Refresh Token 暴露给普通 JavaScript。

Refresh Client

客户端需要解决:

  • concurrent 401 refresh storm;
  • single-flight refresh;
  • refresh failure classification;
  • replay-safe server rotation;
  • retry original request once;
  • definitive invalid_grant → clear local auth state;
  • transient network error ≠ permanent logout。

详见 Access Token / Refresh Token 认证实现流程。

Security Center

前端建议提供独立安全页:

Password / MFA
Active Sessions
Trusted Devices
Login History
Recovery Codes
Recent Security Events

让用户能:

  • revoke session;
  • revoke remembered device;
  • rotate recovery codes;
  • inspect suspicious login;
  • trigger secure-account flow。

安全检查清单

检查项状态说明
Password verifier✅/需演进当前示例 bcrypt;目标支持 Argon2id + rehash-on-login
密码策略需更新长度优先 + breached/common-password blocklist;不再强制大小写/数字组合
在线猜测防御⚠️ 需演进当前有 Redis fixed-window/旧 account counter;目标是 account + source 多维 throttling、progressive delay 与风险 step-up
防邮箱枚举✅/需持续验证登录与 recovery request 都应使用统一公共响应,并关注 timing 差异
Access Token 生命周期需按架构验证应保持短生命周期;具体 TTL 不能机械写死为“7 天即短期”
Refresh Token 旋转✅/需持续验证rotation + replay detection + Session/Grant authority
Session 撤销✅/需统一以 Redis Session Authority / Refresh family 为主,不再以 raw Access Token blacklist 为核心模型
Password Reset Token⚠️ 需演进CSPRNG、digest storage、single-use、purpose-bound、原子消费
Recovery 后撤销⚠️ 需演进reset password 后撤销 Session/Refresh family,重新登录
Email Verification⚠️ 需实现/验证purpose-bound token + pending email + old-email notification
Active Sessions⚠️ 需投影Session/Grant 可枚举、current session、单会话/全会话 revoke
Trusted Device待设计可撤销 remember-device credential;fingerprint/IP 仅作 risk signal
Reauthentication待设计高风险动作检查 auth_time / MFA freshness / assurance
Security Events待设计登录、MFA、Recovery、Credential Change、Session Revoke 统一事件模型
Risk-Based Auth待设计多维信号 → allow / step-up / deny / alert
HTTPS 必须✅Caddy 自动 HTTPS

常见面试问题

Q: 为什么用双 Token 而不是一个?

A:

  • Access Token:用于高频 API fast path,应保持短生命周期,降低自包含凭证的 stale window。
  • Refresh Token / Session Grant:承担长期登录生命周期、rotation、replay detection 与 revoke。
  • 分离关注点:普通请求可以低延迟验证;刷新、登出、设备管理、账号恢复回到服务端 authority。
  • 如果系统直接使用 opaque server-side session,也不一定需要独立“双 Token”架构。

Q: 密码忘了怎么找回?

A:

  1. 用户提交邮箱,公共响应始终保持 generic,避免 account enumeration。
  2. 使用 CSPRNG 生成高熵 reset token;只把 token digest + user/purpose/expiry/status 存 Redis/DB。
  3. 通过可信配置的 HTTPS recovery URL 发邮件;限制 request 与 token verification 频率。
  4. 用户点击后验证 token digest、expiry、purpose,并原子消费为 single-use。
  5. 新密码经过统一 Password Policy 与当前 PasswordVerifier policy。
  6. 更新 verifier 后撤销该用户所有 Session / Refresh families,发送安全通知。
  7. 不自动签发长期登录态;要求用户走正常登录/MFA 流程。

Q: 怎么防止 Token 被盗用?

A:

  1. HTTPS:保护传输。
  2. 短期 Access Token:限制泄漏后的 stale window。
  3. Refresh rotation + replay detection:发现复制/重放后撤销对应 family/session。
  4. Server-side Session Authority:Logout、recovery、账号封禁时提供真正的 revoke 边界。
  5. Credential/security version:在需要时让凭证变更使旧认证状态失效。
  6. 设备/IP 风险信号:用于异常检测和 step-up,不把 IP 绑定当成绝对身份。

常见错误

错误信息泄露用户存在

// ❌ 不同错误返回不同信息
if user == nil {
    c.JSON(401, gin.H{"error": "user not found"})
} else {
    c.JSON(401, gin.H{"error": "wrong password"})
}

// ✅ 统一错误信息
c.JSON(401, gin.H{"error": "invalid email or password"})

只清前端 / 只处理 Access Token

❌ client clears cookie/token
   → server Session 仍然 live

❌ 只维护 raw Access Token blacklist
   → Refresh family / trusted device / Session authority 仍可能有效

更稳定的 Logout / Recovery 模型:

resolve Session / Grant
→ revoke server-side authority
→ revoke Refresh family
→ clear client credential
→ accept or explicitly close Access Token stale window

详见 Credential Change & Session Revocation。

把 bcrypt cost 当成永久常数

// ❌ “cost=12 永远够用”
hash, _ := bcrypt.GenerateFromPassword([]byte(password), 12)

cost 应通过当前硬件、登录并发和延迟预算 benchmark,并允许后续升级。已有较弱 verifier 可以在用户下一次成功登录时 rehash;如果迁移到 Argon2id,还应同时设定 memory/time/parallelism policy。

创建于 2026/6/25 更新于 2026/9/30
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本