BodySense 认证完整流程
BodySense 项目的完整认证流程:注册、登录、Token 刷新、注销、密码重置、邮箱验证的全链路设计。
[!info] related notes
- 知识地图: BodySense 项目 MOC
- JWT: JWT 认证中间件
- Redis: Redis 缓存实践
- 安全: Red Flag 检测
- Password: Password Storage & Verification, Argon2id, bcrypt
- Recovery: Password Reset & Account Recovery, Password Reset Token
- Credential Lifecycle: Credential Change & Session Revocation
- Email: Email Verification & Email Change Security
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 目标模型
当前项目知识中仍存在两类机制:
- Auth Redis fixed-window rate limit,例如 Login 10 / 5 minutes;
- 旧示例中的 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。
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 在:
而不是 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:
- 用户提交邮箱,公共响应始终保持 generic,避免 account enumeration。
- 使用 CSPRNG 生成高熵 reset token;只把 token digest + user/purpose/expiry/status 存 Redis/DB。
- 通过可信配置的 HTTPS recovery URL 发邮件;限制 request 与 token verification 频率。
- 用户点击后验证 token digest、expiry、purpose,并原子消费为 single-use。
- 新密码经过统一 Password Policy 与当前 PasswordVerifier policy。
- 更新 verifier 后撤销该用户所有 Session / Refresh families,发送安全通知。
- 不自动签发长期登录态;要求用户走正常登录/MFA 流程。
Q: 怎么防止 Token 被盗用?
A:
- HTTPS:保护传输。
- 短期 Access Token:限制泄漏后的 stale window。
- Refresh rotation + replay detection:发现复制/重放后撤销对应 family/session。
- Server-side Session Authority:Logout、recovery、账号封禁时提供真正的 revoke 边界。
- Credential/security version:在需要时让凭证变更使旧认证状态失效。
- 设备/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。