密码是系统安全的第一道防线,也是最容易被打穿的一道防线。用户习惯用弱密码、重复密码、键盘连续密码------如果平台不做强制约束,一次撞库攻击就能让整个系统沦陷。本篇记录 DevOps 平台的密码策略模块设计:从策略配置到弱口令管理,从扩展规则到登录防护,如何用一套「端口-适配器」架构把安全能力做成可插拔的基础组件。
一、整体架构:三层分离
密码策略不是一个独立功能,而是一组贯穿「注册 → 登录 → 改密 → 登录防护」全链路的安全能力。平台将它拆成三层:
markdown
modules-core(接口层)
├─ PasswordPolicySnapshot 策略快照(纯 POJO,无持久化注解)
├─ PasswordPolicyProvider 策略提供接口(business 实现)
├─ PasswordSecurityService 密码校验 + 登录防护接口
├─ PasswordCredentialService 写密门面接口(校验 + 哈希 + 强制改密标记)
├─ LoginSecurityFacade 登录安全编排接口
└─ PasswordAccountPort 账号端口接口(business 实现)
│
modules-security(实现层)
├─ DefaultPasswordSecurityService 策略校验 + Redis 登录失败防护
├─ DefaultPasswordCredentialService 写密场景统一处理
├─ DefaultLoginSecurityFacade 登录安全编排
├─ RedisLoginLockStore 登录失败计数 / 临时锁
└─ ForcePwdChangeInterceptor 强制改密拦截器
│
business-system(数据层)
├─ SysPasswordPolicyProvider 读库组装策略快照 + 本地缓存
├─ SysPasswordPolicyController 策略配置 CRUD
├─ SysPasswordWeakWordController 弱口令管理 CRUD
└─ SysPasswordPolicyRuleController 扩展规则管理 CRUD
为什么这样分? modules-core 只定义接口和快照对象,不依赖任何数据库、Redis 或业务模块。modules-security 提供默认实现,依赖 Redis 做登录防护。business-system 负责从数据库读取策略、弱口令、扩展规则,组装成快照供安全模块消费。
如果将来要换掉 Redis(比如改用 Caffeine 做本地缓存),只需要在 modules-security 中替换 RedisLoginLockStore,不影响策略配置和业务逻辑。这就是端口-适配器模式的价值。
二、策略配置
2.1 单记录设计
密码策略是全局唯一的------整个平台只有一份当前生效的策略,不是每个企业一套。sys_password_policy 表通过 LIMIT 1 查询当前策略记录,更新时只允许修改已有记录字段,不允许新增第二条。
这种设计避免了「多条策略到底哪条生效」的歧义,也简化了管理界面------不需要选择策略、不需要设置优先级,配置即生效。
2.2 策略字段总览
策略配置分为五个维度,共 20+ 个可调参数:
基础强度
| 字段 | 默认值 | 说明 |
|---|---|---|
| enabled | true | 策略总开关 |
| minLength | 8 | 最短密码长度 |
| maxLength | 64 | 最长密码长度 |
| requireUpper | true | 必须包含大写字母 |
| requireLower | true | 必须包含小写字母 |
| requireDigit | true | 必须包含数字 |
| requireSpecial | false | 必须包含特殊字符 |
内容约束
| 字段 | 默认值 | 说明 |
|---|---|---|
| forbidUsername | true | 禁止密码包含用户名 |
| maxRepeatChar | 3 | 同一字符连续重复上限 |
| forbidKeyboardSeq | true | 禁止键盘连续序列(qwer、1234 等) |
| forbidCommonWeak | true | 禁止弱口令词库中的密码 |
生命周期
| 字段 | 默认值 | 说明 |
|---|---|---|
| historyCount | 3 | 历史密码不可重复次数 |
| minAgeDays | 0 | 最短改密间隔天数(防止频繁改密) |
| expireDays | 90 | 密码有效天数 |
| expireWarnDays | 7 | 到期前提醒天数 |
| tempPwdExpireHours | 24 | 临时密码有效小时数 |
强制改密
| 字段 | 默认值 | 说明 |
|---|---|---|
| forceChangeFirst | true | 首次登录强制改密 |
| forceChangeReset | true | 管理员重置后强制改密 |
| forceChangeOnPolicy | false | 策略升级后强制改密 |
| pwdChangeKickSession | true | 改密后踢掉其他会话 |
登录防护
| 字段 | 默认值 | 说明 |
|---|---|---|
| maxFailCount | 5 | 最大登录失败次数 |
| failWindowMinutes | 15 | 失败统计窗口 |
| failLockMinutes | 30 | 临时锁定分钟数(0=永久锁定) |
| captchaAfterFails | 3 | 触发验证码的失败次数 |
功能截图:

2.3 策略快照与缓存
策略配置修改后不需要重启服务即可生效。核心机制是 SysPasswordPolicyProvider:
scss
策略读取流程
├─ getCurrent()
│ ├─ AtomicReference 缓存命中 → 直接返回快照
│ └─ 缓存为空 → loadFromDb()
│ ├─ 查 sys_password_policy(LIMIT 1)
│ ├─ 查 sys_password_weak_word(启用的词列表)
│ └─ 查 sys_password_policy_rule(启用的规则 Map)
│ → 组装 PasswordPolicySnapshot
│ → 写入缓存
│
└─ evictCache()
└─ 策略/弱口令/规则 CRUD 后主动清除缓存
→ 下次读取时重新加载
为什么用 AtomicReference 而不是 @Cacheable? 策略快照是一个聚合对象------需要同时读取策略主表、弱口令词库、扩展规则三张表才能组装完成。@Cacheable 适合单方法单 key 的缓存,而这里需要三表联合加载后缓存为一个整体。AtomicReference 配合 evictCache() 主动失效,简单直接。
每次策略修改、弱口令增删改、扩展规则启停,都会调用 evictCache() 清除缓存,下次请求时重新从数据库加载。这保证了配置的实时生效,同时避免了每次校验密码都查三张表的性能开销。
2.4 密码校验链
当用户设置或修改密码时,密码会经过一条完整的校验链:
密码校验链(DefaultPasswordSecurityService.validate)
├─ 策略未启用或为空 → 直接通过
├─ 密码为空 → 失败
├─ 长度校验(minLength / maxLength)
├─ 字符类型校验(大写 / 小写 / 数字 / 特殊)
├─ 禁止包含用户名(forbidUsername)
├─ 连续重复字符校验(maxRepeatChar)
├─ 弱口令精确匹配(forbidCommonWeak → weakWords 列表)
├─ 键盘序列校验(forbidKeyboardSeq → 读取扩展规则或默认序列)
└─ 历史密码校验(historyCount → BCrypt 比对历史哈希)
弱口令匹配策略: 不是模糊包含,而是精确匹配 (全小写对比)。password123 不会被 password 拦截,只有密码本身是 password 时才命中。这避免了误伤------如果用户密码是 Password@2024,虽然包含 password 字段,但整体强度足够,不应被拦截。
键盘序列检测: 默认检测数字序列 0123456789、字母序列 abcdefghijklmnopqrstuvwxyz 以及 QWERTY 键盘的三行 qwertyuiop、asdfghjkl、zxcvbnm。同时检测正向和反向序列(如 4321、trewq)。最小序列长度默认 4,可通过扩展规则自定义。
历史密码校验: 用户改密时传入当前密码哈希列表(BCrypt),按 historyCount 取最近 N 条逐一比对。如果新密码与任何一条历史密码匹配,拒绝修改。BCrypt 比对是安全的------不存储明文,只比对哈希。
三、弱口令管理
3.1 弱口令词库
弱口令管理是一个独立的 CRUD 功能,维护一份「不允许用作密码」的词库。词库中的词条在策略开启 forbidCommonWeak 时生效,用户设置的密码如果与词库中某个词条精确匹配(全小写),则被拒绝。
弱口令实体 SysPasswordWeakWord 包含:
- 弱口令:词条内容,统一小写存储
- 类型 :
builtin(内置)或custom(自定义) - 备注:词条说明
3.2 内置与自定义
词条分为两类:
- 内置词条 :系统初始化时预置的常见弱口令(如
password、admin123、qwerty等)。内置词条不允许修改原文、不允许删除,只能禁用。 - 自定义词条:管理员手动添加或批量导入的词条。可以修改、删除。
这种设计保证了基础弱口令防护不会被误删除------管理员可以禁用某个内置词条(如果业务上有特殊需求),但不能删除它。自定义词条则完全由管理员维护。
3.3 批量导入
弱口令管理支持批量添加,支持换行分隔、逗号分隔、空格分隔。后端会自动规范化(trim + 小写)并去重,已存在的词条自动跳过。批量导入完成后返回成功数、跳过数、总解析数。
scss
批量导入流程
├─ 接收原始文本(换行 / 逗号 / 空格分隔)
├─ 逐行拆分 + trim + 小写规范化
├─ LinkedHashSet 去重(保序)
├─ 逐条检查是否已存在
│ ├─ 已存在 → skipped++
│ └─ 不存在 → 插入数据库 → success++
├─ 若 success > 0 → evictCache() 清除策略缓存
└─ 返回 BatchResult(success / skipped / total)
功能截图:

3.4 缓存联动
弱口令的任何增删改操作都会触发 SysPasswordPolicyProvider.evictCache(),清除策略快照缓存。下次密码校验时重新加载弱口令列表。这保证了新增弱口令后立即生效,不需要重启服务。
四、扩展规则
4.1 为什么需要扩展规则
策略主表覆盖的是通用规则(长度、字符类型、弱口令等),但有些规则需要更灵活的配置------比如键盘序列的自定义模式、渐进式锁定策略、相似度检测等。这些规则的配置格式各不相同,不适合用固定字段表示。
扩展规则设计为 ruleCode → ruleValue(JSON) 的键值对结构,ruleValue 是 JSON 字符串,由安全模块按 ruleCode 解析。这样既保持了策略主表的简洁,又提供了无限的扩展能力。
4.2 扩展规则实体
扩展规则实体 SysPasswordPolicyRule 包含:
- 规则编码 :全局唯一,一旦创建不允许修改(与策略编码、企业编码等设计一致)
- 规则名称:展示用
- 启用状态:可启停
- 优先级:控制规则排序
- 规则配置:JSON 格式,按规则编码有不同的字段结构
- 备注:规则说明
规则编码不可修改------编码是安全模块解析规则的 key,修改编码会导致规则与实现脱节。
4.3 已实现的扩展规则
当前已实现三种扩展规则:
keyboard_seq --- 键盘序列检测
json
{
"minLen": 4,
"patterns": ["qwertyuiop", "asdfghjkl", "zxcvbnm", "0123456789"]
}
minLen:最小序列长度,低于此长度的子串不检测patterns:自定义键盘序列模式列表
如果扩展规则未配置或解析失败,回退到默认的五组序列(数字、字母、QWERTY 三行)。
progressive_lock --- 渐进式锁定
json
{
"steps": [
{ "fails": 3, "lockMinutes": 5 },
{ "fails": 5, "lockMinutes": 15 },
{ "fails": 8, "lockMinutes": 60 }
]
}
不同于策略主表中的 maxFailCount + failLockMinutes(一次性锁定),渐进式锁定支持多档阶梯------失败 3 次锁 5 分钟,失败 5 次锁 15 分钟,失败 8 次锁 1 小时。越往后锁定时间越长,有效抑制暴力破解。
similar_check --- 密码相似度检测
json
{
"enabled": true,
"maxSimilarity": 0.7
}
检测新密码与旧密码的相似度,超过阈值则拒绝修改。防止用户把 Password@1 改成 Password@2 来绕过历史密码校验。
功能截图:




4.4 规则解析的安全兜底
扩展规则的 ruleValue 是 JSON 字符串,解析时可能出现格式错误。安全模块的处理策略是:解析失败时回退默认值 。比如 keyboard_seq 规则 JSON 解析失败,自动使用默认的五组键盘序列。这保证了规则配置错误不会导致密码校验功能整体不可用。
javascript
扩展规则解析流程(以 keyboard_seq 为例)
├─ 从策略快照读取 rules.get("keyboard_seq")
├─ ruleValue 为空?
│ └─ 是 → 使用默认五组键盘序列
├─ JSON.parse(ruleValue)
│ ├─ 解析成功
│ │ ├─ 提取 minLen(默认 4,最小 2)
│ │ └─ 提取 patterns 数组(替换默认序列)
│ └─ 解析失败(JSON 格式错误)
│ └─ 静默忽略 → 回退默认五组序列
└─ 逐 pattern 滑动窗口检测(正向 + 反向)
└─ 密码包含序列子串 → 拦截
五、登录防护
5.1 登录时密码验证全流程
用户从输入密码到最终登录成功,密码要经历「身份验证 → 强度策略校验 → 登录后评估」三个阶段。下面是一张完整流程图,串联了密码校验链、BCrypt 比对、登录后策略评估和强制改密拦截:
scss
登录时密码验证全流程
│
├─ ① 身份验证阶段
│ ├─ 用户提交 username + password
│ ├─ assertNotTempLocked(username)
│ │ ├─ Redis lock key 存在 → 拒绝「账号已锁定,N 秒后重试」
│ │ └─ 未锁定 → 继续
│ ├─ needCaptcha(username)?
│ │ ├─ 是 → 校验验证码 → 失败则拒绝
│ │ └─ 否 → 继续
│ ├─ PasswordAccountPort.findByUsername(username)
│ │ ├─ 用户不存在 → onLoginFail(username, null) → 返回错误
│ │ └─ 用户存在 → 取出 passwordHash
│ ├─ BCrypt.checkpw(plainPassword, passwordHash)
│ │ ├─ 不匹配 → onLoginFail(username, userId) → 累计失败/锁定 → 返回错误
│ │ └─ 匹配 → 进入 ②
│
├─ ② 密码强度策略校验阶段(登录后评估)
│ ├─ onLoginSuccess(username) → 清除 Redis fail/lock key
│ ├─ evaluatePostLoginPolicy(userId, username, plainPassword)
│ │ ├─ 读取当前策略快照(PasswordPolicySnapshot)
│ │ ├─ 用当前策略重新校验本次登录密码
│ │ │ ├─ 长度 / 字符类型 / 用户名 / 重复字符
│ │ │ ├─ 弱口令精确匹配
│ │ │ ├─ 键盘序列检测
│ │ │ └─ (登录时不校验历史密码,只校验当前密码是否符合策略)
│ │ ├─ 校验结果
│ │ │ ├─ 通过 + 无强制标记 → 正常签发 Token
│ │ │ ├─ 通过 + 有强制标记 → 签发 Token + 前端提示改密
│ │ │ └─ 不通过 → setForcePwdChange(userId, true) → 签发 Token + 前端强制改密
│ │ └─ 返回 PostLoginPolicyResult(needChangePwd + changePwdReason)
│ └─ 签发 Sa-Token JWT
│
└─ ③ 后续请求拦截阶段
├─ ForcePwdChangeInterceptor.preHandle()
│ ├─ 未登录 → 放行
│ ├─ 已登录 + 无强制标记 → 放行
│ └─ 已登录 + 有强制标记
│ ├─ 请求在白名单(login / change-pwd / logout / info)→ 放行
│ └─ 请求不在白名单 → 抛 NEED_CHANGE_PWD → 前端跳转改密页
└─ 用户改密成功 → setForcePwdChange(userId, false) → 恢复正常访问
三个阶段的关系:
- ① 身份验证:回答「这个密码对不对」------BCrypt 比对数据库中的哈希。
- ② 策略校验:回答「这个密码够不够安全」------即使用户知道密码(BCrypt 匹配),但如果密码不符合当前策略(比如策略升级了 minLength),登录后立即标记强制改密。
- ③ 拦截防护:回答「不安全的密码能不能用系统」------被标记强制改密的用户,除改密/登出外的一切请求都被拦截器拦住。
这种设计确保了一个关键安全属性:用户能用旧密码登录(不断服务),但登录后必须立即改密码。如果直接拒绝登录,用户会被困在外面无法自助改密;如果放行不管,弱密码就一直存在。登录后评估 + 拦截器是两者的平衡。
5.2 登录失败计数
登录失败防护由 RedisLoginLockStore 实现,使用 Redis 的两个 key 模式:
vbnet
security:pwd:fail:{username} → 失败计数(INCR,窗口内有效)
security:pwd:lock:{username} → 临时锁定标记(SET + TTL)
登录防护的完整链路从用户提交登录请求开始,一直到最终返回结果:
scss
登录安全全流程
├─ 用户提交登录请求
├─ assertNotTempLocked(username)
│ ├─ Redis lock key 存在?
│ │ ├─ 是 → 返回「账号已锁定,N 秒后重试」
│ │ └─ 否 → 继续
├─ needCaptcha(username)?
│ └─ 是 → 校验验证码
├─ 校验账号密码
│ ├─ 密码正确
│ │ ├─ onLoginSuccess → 清除 fail key + lock key
│ │ ├─ evaluatePostLoginPolicy → 评估密码是否符合当前策略
│ │ │ ├─ 符合 → 正常登录,签发 Token
│ │ │ └─ 不符合 → 打强制改密标记 → 前端跳转改密页
│ │ └─ 签发 Token
│ └─ 密码错误
│ ├─ onLoginFail(username)
│ │ ├─ INCR fail key(首次设窗口 TTL)
│ │ ├─ failCount >= captchaAfterFails → 标记需验证码
│ │ ├─ failCount >= maxFailCount
│ │ │ ├─ failLockMinutes > 0 → SET lock key(TTL = failLockMinutes)→ 临时锁定
│ │ │ └─ failLockMinutes = 0 → lockForPasswordFail → 永久锁定
│ │ └─ 返回 LoginFailOutcome
│ └─ 返回错误信息(含验证码 / 锁定提示)
└─ 前端根据结果展示(验证码 / 锁定倒计时 / 登录成功)
窗口机制: 失败计数的 Redis key 在首次失败时设置过期时间(failWindowMinutes,默认 15 分钟)。如果用户在 15 分钟内没有再失败,计数自动归零。如果持续失败,计数不重置------因为每次 INCR 都不会刷新 TTL,窗口是固定的。
5.3 临时锁定与永久锁定
- 临时锁定 :达到
maxFailCount后,在 Redis 中写入 lock key,TTL 为failLockMinutes分钟。锁定期间登录直接拒绝,不需要校验密码。锁定到期后自动解除。 - 永久锁定 :当
failLockMinutes配置为 0 时,不使用 Redis 临时锁,而是通过PasswordAccountPort.lockForPasswordFail()写入数据库持久锁定。永久锁定需要管理员手动解锁。
5.4 登录成功后清理
登录成功时,onLoginSuccess 清除该用户的失败计数和临时锁 key。这保证了用户偶尔输错密码不会累积------只要最终登录成功,计数归零。
5.5 登录后策略评估
登录成功后不只是发 Token------安全模块还会做一次「登录后策略评估」:
登录后策略评估(evaluatePostLoginPolicy)
├─ 查询用户账号信息
├─ 检查是否已有强制改密标记(forcePwdChange)
├─ 用当前策略校验本次登录密码
│ ├─ 校验通过 + 无强制标记 → 正常登录
│ ├─ 校验不通过 → 打强制改密标记 + 返回改密原因
│ └─ 已有强制标记 → 直接返回改密原因
└─ 返回 PostLoginPolicyResult(是否需要改密 + 改密原因)
为什么登录后还要校验密码? 策略是可变的------管理员今天把 minLength 从 8 改成 12,用户原来的 8 位密码就不符合新策略了。登录后评估能发现这种情况,强制用户改密,确保所有在线密码始终符合当前策略。
5.6 强制改密拦截器
当用户被标记为需要强制改密时,ForcePwdChangeInterceptor 会拦截除改密、登出、查询信息之外的所有请求:
bash
ForcePwdChangeInterceptor
├─ 未登录 → 放行
├─ 已登录但未标记强制改密 → 放行
└─ 已登录且标记强制改密
├─ 请求路径在白名单内(login / change-pwd / logout / info 等)→ 放行
└─ 请求路径不在白名单内 → 抛出 NEED_CHANGE_PWD 异常
→ 前端收到后跳转强制改密页面
这保证了被标记的用户只能改密码或登出,不能访问任何业务功能。
5.7 改密场景与强制标记
写密分三种场景,每种场景对强制改密标记的处理不同:
| 场景 | 说明 | 强制改密标记 |
|---|---|---|
| FIRST_SET | 新增用户时设置初始密码 | 由 forceChangeFirst 决定 |
| ADMIN_RESET | 管理员重置用户密码 | 由 forceChangeReset 决定 |
| SELF_CHANGE | 用户自助修改密码 | 强制设为 false(改完即解除) |
管理员重置密码后,用户下次登录时被拦截器拦住,必须先改一个自己记住的新密码才能使用系统。这是常见的安全实践------管理员给的临时密码不应该长期使用。
六、端口-适配器:安全模块如何与业务解耦
6.1 PasswordAccountPort
安全模块需要读写用户账号信息(密码哈希、强制改密标记、锁定状态),但不依赖业务模块的 SysUser 实体。通过 PasswordAccountPort 接口抽象:
findByUsername(userId)→ 查询账号安全视图updatePassword(userId, hash, force, time)→ 更新密码setForcePwdChange(userId, force)→ 设置强制改密标记lockForPasswordFail(userId, reason)→ 永久锁定
business-system 实现 PasswordAccountPort,内部对接 SysUser 和 SysUserLock 表。安全模块只通过接口操作,不知道也不关心底层表结构。
scss
端口-适配器调用关系
modules-security(安全模块)
│
├── PasswordSecurityService
│ ├─ validate() → 校验密码强度
│ ├─ onLoginFail() → Redis 计数 + 锁定
│ └─ onLoginSuccess() → Redis 清理
│
├── PasswordCredentialService
│ ├─ prepareNewPassword() → 校验 + BCrypt 哈希
│ └─ applyNewPassword() → 校验 + 哈希 + 调 Port 落库
│
├── LoginSecurityFacade
│ ├─ assertNotTempLocked() → 查 Redis lock
│ ├─ onBadCredentials() → 调 onLoginFail + 永久锁定
│ └─ evaluatePostLoginPolicy() → 校验密码 + 打标记
│
└── ForcePwdChangeInterceptor
└─ accountPort.isForcePwdChange() → 拦截请求
│
▼ (接口边界)
business-system(业务模块)
│
├── PasswordAccountPort 实现
│ ├─ findById() → 查 sys_user
│ ├─ updatePassword() → 更新 sys_user.password
│ ├─ setForcePwdChange() → 更新 sys_user.force_pwd_change
│ └─ lockForPasswordFail()→ 插入 sys_user_lock
│
└── PasswordPolicyProvider 实现
└─ getCurrent() → 查三表 + 组装快照 + 缓存
6.2 ObjectProvider 的柔性注入
modules-security 在注入 PasswordPolicyProvider 和 PasswordAccountPort 时使用 ObjectProvider 而非直接 @Autowired:
- 如果 business-system 模块存在(正常部署场景),Provider 和 Port 都可用,安全功能完整运行。
- 如果 business-system 模块不存在(比如安全模块被其他项目复用),安全模块不会启动失败,只是策略校验功能降级(无策略 → 直接通过)。
这种柔性设计让安全模块可以被任何项目复用,只需要业务端实现对应的 Port 即可。
七、总结与思考
做对了什么
-
策略快照 + 本地缓存 + 主动失效。三表联合加载后缓存为一个整体,配置修改后主动清除缓存即时生效。既保证了性能(每次校验不查库),又保证了实时性。
-
端口-适配器彻底解耦 。安全模块通过
PasswordAccountPort操作账号,不依赖任何业务实体。安全模块可以被任何项目复用,业务端只需要实现 Port 接口。 -
弱口令精确匹配而非模糊包含。避免了「密码包含弱口令子串」的误伤,只拦截整体等于弱口令的情况,体验和安全兼顾。
-
登录后策略评估。策略升级后,老密码不符合新策略的用户会在下次登录时被发现,强制改密。不会出现「策略改了但老密码一直能用」的安全漏洞。
-
扩展规则 JSON 配置 + 安全兜底 。
keyboard_seq等规则支持自定义 JSON 配置,解析失败时回退默认值。既灵活又健壮。 -
ObjectProvider 柔性注入。安全模块不强制依赖业务模块的存在,可独立复用。
可以改进的地方
-
历史密码只存当前一条 。当前
PasswordAccountPort.updatePassword只传入当前密码哈希,历史密码校验只能比一条。要真正支持historyCount=3,需要单独维护一张密码历史表,每次改密时插入旧密码哈希。 -
密码过期提醒未实现 。策略中有
expireDays和expireWarnDays字段,但当前没有定时任务检查密码是否即将过期。需要在定时任务模块中加一个每日扫描,对即将过期的用户通过 SSE 推送提醒。 -
临时密码功能未完整 。
tempPwdExpireHours字段已定义,但临时密码的生成、分发、过期检查流程尚未完整实现。后续需要配合通知模块(邮件/短信)完成。 -
验证码触发后未持久化 。当前
captchaAfterFails只在 Redis 中计数,如果 Redis 重启,计数归零。对于安全要求更高的场景,可以考虑将失败计数持久化到数据库。 -
渐进式锁定与一次性锁定的关系不清晰 。
progressive_lock扩展规则和策略主表的maxFailCount+failLockMinutes是两套独立的逻辑,当前progressive_lock规则虽然定义了配置结构,但安全模块的onLoginFail仍然走的是主表的一次性锁定逻辑。后续需要让progressive_lock规则真正参与登录失败处置。
2026 年 8 月 · 第四篇