1. Demand analysis
Under certain sensitive operations, we need to perform secondary authentication on the logged-in session.
For example, the warehouse deletion operation of the code hosting platform, although we have already logged in the account, when we click the [Delete] button, we still need to enter the password again. This is mainly for two points:
- Make sure that the operator is the current account owner.
- Added operation steps to prevent accidental deletion of important data.
This is what we are going to talk about in this article—two-level authentication, that is, to re-authenticate on the basis of the logged-in session to improve the security of the session.
Sa-Token is a lightweight java authority authentication framework, which mainly solves a series of authority-related issues such as login authentication, authority authentication, single sign-on, OAuth2, and microservice gateway authentication. Gitee open source address: https://gitee.com/dromara/sa-token
This article will introduce how to use Sa-Token to complete the secondary authentication operation under the SpringBoot architecture.
First introduce the Sa-Token dependency in the project:
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.34.0</version>
</dependency>
Note: If you are using SpringBoot 3.x
, just sa-token-spring-boot-starter
change to sa-token-spring-boot3-starter
.
2. Specific APIs
It is very simple to do two-level authentication in Sa-Token
, just use the following API:
// 在当前会话 开启二级认证,时间为120秒
StpUtil.openSafe(120);
// 获取:当前会话是否处于二级认证时间内
StpUtil.isSafe();
// 检查当前会话是否已通过二级认证,如未通过则抛出异常
StpUtil.checkSafe();
// 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)
StpUtil.getSafeTime();
// 在当前会话 结束二级认证
StpUtil.closeSafe();
3. A small example
A complete secondary authentication business process should be roughly as follows:
package com.pj.cases.up;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
/**
* Sa-Token 二级认证示例
*
* @author kong
* @since 2022-10-16
*/
@RestController
@RequestMapping("/safe/")
public class SafeAuthController {
// 删除仓库 ---- http://localhost:8081/safe/deleteProject
@RequestMapping("deleteProject")
public SaResult deleteProject(String projectId) {
// 第1步,先检查当前会话是否已完成二级认证
// 这个地方既可以通过 StpUtil.isSafe() 手动判断,
// 也可以通过 StpUtil.checkSafe() 或者 @SaCheckSafe 来校验(校验不通过时将抛出 NotSafeException 异常)
if(!StpUtil.isSafe()) {
return SaResult.error("仓库删除失败,请完成二级认证后再次访问接口");
}
// 第2步,如果已完成二级认证,则开始执行业务逻辑
// ...
// 第3步,返回结果
return SaResult.ok("仓库删除成功");
}
// 提供密码进行二级认证 ---- http://localhost:8081/safe/openSafe?password=123456
@RequestMapping("openSafe")
public SaResult openSafe(String password) {
// 比对密码(此处只是举例,真实项目时可拿其它参数进行校验)
if("123456".equals(password)) {
// 比对成功,为当前会话打开二级认证,有效期为120秒,意为在120秒内再调用 deleteProject 接口都无需提供密码
StpUtil.openSafe(120);
return SaResult.ok("二级认证成功");
}
// 如果密码校验失败,则二级认证也会失败
return SaResult.error("二级认证失败");
}
// 手动关闭二级认证 ---- http://localhost:8081/safe/closeSafe
@RequestMapping("closeSafe")
public SaResult closeSafe() {
StpUtil.closeSafe();
return SaResult.ok();
}
}
Global exception interceptor, uniformly returned to the front-end format, refer to:
@RestControllerAdvice
public class GlobalExceptionHandler {
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
Premise: We must be in the login state during the second-level authentication (refer to the previous login authentication chapter code), after completing the login, call the interface to perform the second-level authentication verification:
- The front end calls the deleteProject interface to try to delete the warehouse. ----
http://localhost:8081/safe/deleteProject
- The backend verification session has not yet completed the second-level authentication, and returns: Failed to delete the warehouse, please access the interface again after completing the second-level authentication.
- The front end prompts the information to the user, the user enters the password, and calls the openSafe interface. ----
http://localhost:8081/safe/openSafe?password=123456
- The backend compares the password entered by the user to complete the secondary authentication, and the validity period is: 120 seconds.
- The front end calls the deleteProject interface again within 120 seconds to try to delete the warehouse. ----
http://localhost:8081/safe/deleteProject
- The backend verifies that the session has completed the second-level authentication, and returns: the warehouse is deleted successfully.
4. Specify the business logo for secondary certification
If multiple business lines of the project require sensitive operation verification, StpUtil.openSafe()
fine-grained authentication operations cannot be provided. At this time, we can specify a business identifier to distinguish different business lines:
// 在当前会话 开启二级认证,业务标识为client,时间为600秒
StpUtil.openSafe("client", 600);
// 获取:当前会话是否已完成指定业务的二级认证
StpUtil.isSafe("client");
// 校验:当前会话是否已完成指定业务的二级认证 ,如未认证则抛出异常
StpUtil.checkSafe("client");
// 获取当前会话指定业务二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)
StpUtil.getSafeTime("client");
// 在当前会话 结束指定业务标识的二级认证
StpUtil.closeSafe("client");
The business ID can be filled with any string, and the authentication between different business IDs does not affect each other, for example:
// 打开了业务标识为 client 的二级认证
StpUtil.openSafe("client");
// 判断是否处于 shop 的二级认证,会返回 false
StpUtil.isSafe("shop"); // 返回 false
// 也不会通过校验,会抛出异常
StpUtil.checkSafe("shop");
Code example:
package com.pj.cases.up;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
/**
* Sa-Token 二级认证示例
*
* @author kong
* @since 2022-10-16
*/
@RestController
@RequestMapping("/safe/")
public class SafeAuthController {
// ------------------ 指定业务类型进行二级认证
// 获取应用秘钥 ---- http://localhost:8081/safe/getClientSecret
@RequestMapping("getClientSecret")
public SaResult getClientSecret() {
// 第1步,先检查当前会话是否已完成 client业务 的二级认证
StpUtil.checkSafe("client");
// 第2步,如果已完成二级认证,则返回数据
return SaResult.data("aaaa-bbbb-cccc-dddd-eeee");
}
// 提供手势密码进行二级认证 ---- http://localhost:8081/safe/openClientSafe?gesture=3578
@RequestMapping("openClientSafe")
public SaResult openClientSafe(String gesture) {
// 比对手势密码(此处只是举例,真实项目时可拿其它参数进行校验)
if("3578".equals(gesture)) {
// 比对成功,为当前会话打开二级认证:
// 业务类型为:client
// 有效期为600秒==10分钟,意为在10分钟内,调用 getClientSecret 时都无需再提供手势密码
StpUtil.openSafe("client", 600);
return SaResult.ok("二级认证成功");
}
// 如果密码校验失败,则二级认证也会失败
return SaResult.error("二级认证失败");
}
// 查询当前会话是否已完成指定的二级认证 ---- http://localhost:8081/safe/isClientSafe
@RequestMapping("isClientSafe")
public SaResult isClientSafe() {
return SaResult.ok("当前是否已完成 client 二级认证:" + StpUtil.isSafe("client"));
}
}
5. Use annotations for secondary authentication
Use annotations on a method @SaCheckSafe
to perform a secondary authentication check before the code enters this method
// 二级认证:必须二级认证之后才能进入该方法
@SaCheckSafe
@RequestMapping("add")
public String add() {
return "用户增加";
}
// 指定业务类型,进行二级认证校验
@SaCheckSafe("art")
@RequestMapping("add2")
public String add2() {
return "文章增加";
}
The method marked @SaCheckSafe
must be accessed successfully after the second-level authentication, and its effect is StpUtil.checkSafe()
equivalent to the code.
References
- Sa-Token statement: https://sa-token.cc
- Gitee warehouse address: https://gitee.com/dromara/sa-token
- GitHub warehouse address: https://github.com/dromara/sa-token