一、改密码之后,旧令牌必须立刻失效
用 JWT 做鉴权有一个天然缺陷:令牌签发后,在过期之前服务端不存储它,也就无法主动作废。典型的危险场景是:
- 员工账号被盗,管理员改了密码,但攻击者手里的旧令牌仍然有效;
- 员工离职,账号被禁用,但他本地保存的令牌还能继续调接口。
这套系统用令牌版本号解决:账号表上存一个整数版本,签发令牌时把它写进令牌声明里;每次鉴权比对"令牌里的版本"与"账号当前版本",不一致即拒绝。
需要吊销时,只要把账号的版本号 +1:
- 改密码 → 版本 +1,所有旧令牌立即失效;
- 禁用账号 → 版本 +1,同时状态置为禁用;
- 强制下线 → 版本 +1。
这个方案不引入 Redis 等额外依赖,代价是每次鉴权多一次索引查询(按账号业务号取版本),在本系统的量级下完全可接受。
二、登录日志要能回答"谁在什么时候失败过"
很多系统只记录成功的登录,出了问题却查不到"连续失败 20 次"这种关键线索。所以登录日志是只追加的独立表,与后台操作日志分开:
| 表 | 记录什么 | 典型用途 |
|---|---|---|
| 登录日志 | 登录成功/失败、IP、UA、失败原因 | 排查撞库、异地登录、账号锁定 |
| 操作日志 | 后台的增删改、请求参数摘要 | 追责与回滚依据 |
两张表分开的原因是写入量与查询场景完全不同:登录日志写多读少且必须长期保留,操作日志读多且需要按业务筛选。混在一张表里,两边都会被拖慢。
配套的规则是只追加、不更新。日志一旦可编辑就失去证据价值。
三、枚举字典化:文案不该写在前端
早期开发时状态文案直接写在前端:status === 1 ? '待发货' : ...。这种写法在三个地方会立刻反噬:
- 新增一个状态,要改多个页面;
- 同一个状态在不同页面文案不一致;
- 客户要求改成自己的行业叫法("待发货"改成"待出库"),要重新发版。
处理方式是字典化:枚举的 code 留在代码里(判断逻辑稳定),文案放进字典表,前端按 code 取 label。这次把系统域的 12 组枚举(账号状态、登录结果、任务状态、文件类型等)统一搬进了字典表,并做了默认值初始化。
配套的还有字典项颜色:状态标签的颜色也由字典配置(成功/警告/危险/信息),避免每个页面自己挑颜色。
四、这些细节的共同点
三件事看起来不相关,但都属于同一类:它们不影响功能,只影响信任。
- 会话吊销:决定客户敢不敢把这套系统交给几十个员工使用;
- 登录留痕:决定出了安全事件时能不能查;
- 字典化:决定客户提"改个叫法"时是十分钟还是两周。
功能做出来是及格,这些细节做到位才是可交付。这也是为什么我们把"有表无功能"(建了表却没有对应入口)当成 P1 缺陷逐项清零——表存在但没人能操作,对客户来说就是那句话:"你们说支持,但我在后台找不到。"