
safe-auth,都会有一个感觉:概念我懂了,但真到项目里,还是不知道这几个 API 到底该放哪儿。
尤其是这句:
openSafe(...) 在当前会话开启二级认证。
很多人看到这里,还是会懵。 它到底是“校验密码”的方法,还是“校验通过后打标记”的方法? 它应该放在删除接口里,还是放在校验密码的接口里?
这篇我不空讲概念。 我直接拿一个最典型的场景来讲透:删除项目。
如果你只记住一句话,那就是:
safe-auth 解决的不是“用户有没有登录”,而是“用户已经登录了,但执行危险操作前,要不要再确认一次”。
而 openSafe(...) 做的事情也很明确:
它不会帮你校验密码。 它只是你在“密码已经校验通过之后”,给当前 token 写入一个短时有效的二级认证标记。
这句话如果还是有点抽象,没关系,下面直接看代码。
很多人第一次接 safe-auth,很容易写出下面这种代码:
1
2
3
4
5
6
7
8
@DeleteMapping("/project/{id}")
public SaResult deleteProject(@PathVariable Long id) {
// 错误写法:这行代码不会校验密码
StpUtil.openSafe("delete-project", 120);
projectService.deleteById(id);
return SaResult.ok();
}
这段代码的问题非常明显:
你本来是想“删除前再确认一次身份”,结果你却在删除接口里直接执行了 openSafe(...)。
这相当于谁调用了这个接口,谁就被你当场发了一张“已经完成二级认证”的通行证。
也就是说:
它只是单纯地把当前会话标记成“接下来 120 秒内,这个业务已经通过二级认证”
所以这里就能看懂那句官方语义了:
openSafe(...) 不是“验证动作本身”,而是“验证通过后,给当前会话打一个短时有效的安全标记”。
以“删除项目”为例,正确流程不是一个接口做完,而是拆成两个动作:
比如前端点“删除项目”按钮后,不是立刻删,而是先弹窗要求输入登录密码。
这时后端可以提供一个“删除前确认”的接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
@PostMapping("/project/delete-check")
public SaResult deleteCheck(@RequestBody DeleteCheckDTO dto) {
// 1. 先拿到当前登录用户
Object loginId = StpUtil.getLoginId();
// 2. 校验用户输入的密码是否正确
userService.checkPassword(loginId, dto.getPassword());
// 3. 校验通过后,给 delete-project 这个业务开启 120 秒安全期
StpUtil.openSafe("delete-project", 120);
return SaResult.ok("校验成功,120 秒内允许删除项目");
}
注意,这里真正完成“身份确认”的,不是 openSafe(...),而是:
1
userService.checkPassword(loginId, dto.getPassword());
而 openSafe("delete-project", 120) 的作用是:
“既然密码已经验证过了,那我给这个登录会话加一个临时标记。接下来 120 秒内,这个用户可以执行 delete-project 这个高风险业务,不需要再输一次密码。”
这就叫“短时安全状态”。
真正的删除接口应该只关心一件事: 你当前会话有没有通过这个业务的二级认证。
可以这样写:
1
2
3
4
5
6
@SaCheckSafe("delete-project")
@DeleteMapping("/project/{id}")
public SaResult deleteProject(@PathVariable Long id) {
projectService.deleteById(id);
return SaResult.ok("删除成功");
}
或者不用注解,直接手动写:
1
2
3
4
5
6
@DeleteMapping("/project/{id}")
public SaResult deleteProject(@PathVariable Long id) {
StpUtil.checkSafe("delete-project");
projectService.deleteById(id);
return SaResult.ok("删除成功");
}
这里 checkSafe("delete-project") 的意思也很具体:
到这里,这套流程就彻底讲清楚了:
checkPassword(...) 负责“验人”openSafe(...) 负责“打标”checkSafe(...) 负责“查标并放行”很多人会忽略 delete-project 这个字符串,以为只是随便写写。
其实它很重要。 它代表的是:这次二级认证是给哪一类危险动作开的通行证。
比如一个后台系统里,可能同时有这几类高风险操作:
delete-projectreset-api-keyexport-salary这三类动作风险都很高,但风险性质完全不同。 你刚刚为了“删除项目”输入过一次密码,不代表你就应该顺便拿到“导出工资表”的权限。
所以更合理的做法是把它们拆开:
1
2
3
StpUtil.openSafe("delete-project", 120);
StpUtil.openSafe("reset-api-key", 120);
StpUtil.openSafe("export-salary", 120);
对应校验也拆开:
1
2
3
StpUtil.checkSafe("delete-project");
StpUtil.checkSafe("reset-api-key");
StpUtil.checkSafe("export-salary");
这样一来,二级认证就不是“全站一次通过到处通行”,而是“哪个危险动作验证过,就只放行哪个危险动作”。
这个颗粒度才是安全的。
getSafeTime() 在实际项目里有什么用这个 API 很多人也知道名字,但不知道拿来干嘛。
它的典型用途其实很简单: 给前端显示“安全状态还剩多少秒”。
例如:
1
long safeTime = StpUtil.getSafeTime("delete-project");
如果返回值大于 0,说明当前这个业务还在二级认证有效期内。
如果返回 -2,按照官方实现,说明当前还没有通过这项二级认证。
这时候前端可以做得非常具体:
120:弹一句“你已完成身份确认,120 秒内可直接删除项目”35:提示“剩余 35 秒,无需重复验证”-2:直接弹密码框,不要让用户点了删除才发现不让删这样用户体验会顺很多。
safe-auth这里不能再写成“2 到 3 个接口”这种模糊建议了,因为真正的判断标准不是数量,而是风险等级。
更准确的说法应该是:
先把你系统里做错一次代价明显很高的接口单独列出来,再逐个接入。
最常见的一批就是这些:
DELETE /project/{id}DELETE /member/{id}DELETE /config/{id}这类接口的特点是:一旦执行成功,通常不可逆,或者恢复成本很高。
POST /api-key/resetPOST /user/password/resetPOST /pay-secret/reset这类接口的特点是:操作成功后,安全状态会发生变化,影响后续系统访问。
GET /salary/exportGET /user/privacy/exportGET /finance/bill/export这类接口的特点是:虽然不是“修改数据”,但一旦导出成功,损失同样可能很大。
POST /admin/impersonatePOST /batch/approvePOST /batch/disable-user这类接口的特点是:单次操作影响范围大,风险不只在当前用户自己身上。
反过来说,像下面这些接口通常就不值得上:
GET /project/listGET /project/{id}POST /profile/update除非你的业务特别敏感,否则普通查阅、低风险编辑没必要每次都二次确认。
如果你把上面的内容串起来,一个完整的“删除项目”链路大概会长这样:
前端不是直接调:
1
DELETE /project/1001
而是先弹窗,要求输入登录密码。
1
POST /project/delete-check
请求体:
1
2
3
{
"password": "123456"
}
后端校验密码通过后:
1
StpUtil.openSafe("delete-project", 120);
1
DELETE /project/1001
后端入口校验:
1
StpUtil.checkSafe("delete-project");
如果通过,就删。 如果没通过,就拦。
这套流程最大的好处是: 二级认证变成了一层独立能力,而不是散落在每个业务方法里的临时判断。
如果项目已经比较大,我更建议用注解:
1
2
3
4
5
6
7
8
@SaCheckLogin
@SaCheckPermission("project:delete")
@SaCheckSafe("delete-project")
@DeleteMapping("/project/{id}")
public SaResult deleteProject(@PathVariable Long id) {
projectService.deleteById(id);
return SaResult.ok();
}
这段代码一眼就能看出三层门:
比你把判断逻辑散落在 service 里更清晰,也更不容易漏。
最后把最关键的点收一下:
openSafe(...) 不负责验密码,它负责在“验完之后”给当前会话写入一个短时有效的安全标记。checkSafe(...) 才是危险接口入口要做的事,它负责判断这次操作有没有拿到对应业务的通行证。如果你把 safe-auth 理解成一句更白的话,我觉得就是:
登录只证明“这个人进来了”,而二级认证证明“这个人现在真的要做这件危险的事”。
StpLogic 源码:https://gitee.com/dromara/sa-token/blob/8bc66b5028fe071156add010d7238c3d4afe1a02/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.javaSaCheckSafe 处理器源码检索结果:https://gitextract.com/dromara/Sa-Token