Sa-Token Notes(1) 基础篇
2023-08-09 14:53:19 # Tools # Sa-Token

官方文档http://sa-token.dev33.cn/

1 快速开始

整合示例在官方仓库的/sa-token-demo/sa-token-demo-springboot文件夹下,如遇到难点可结合源码进行学习测试。

https://github.com/dromara/sa-token

1.1 创建项目

在 IDE 中新建一个 SpringBoot 项目,例如:sa-token-demo-springboot

1.2 添加依赖

在项目中直接通过 pom.xml 引入 Sa-Token 的依赖即可

1
2
3
4
5
6
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.30.0</version>
</dependency>

1.3 设置配置文件

可以零配置启动项目 ,但同时也可以在 application.yml 中增加如下配置,定制性使用框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server:
# 端口
port: 8081

# Sa-Token配置
sa-token:
# token 名称 (同时也是cookie名称)
token-name: satoken
# token 有效期,单位s 默认30天, -1代表永不过期
timeout: 2592000
# token 临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
activity-timeout: -1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: false
# token风格
token-style: uuid
# 是否输出操作日志
is-log: false

1.4 创建项目

在项目中新建包 com.pj ,在此包内新建主类 SaTokenDemoApplication.java

1
2
3
4
5
6
7
@SpringBootApplication
public class SaTokenDemoApplication {
public static void main(String[] args) throws JsonProcessingException {
SpringApplication.run(SaTokenDemoApplication.class, args);
System.out.println("启动成功:Sa-Token配置如下:" + SaManager.getConfig());
}
}

1.5 创建测试Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@RequestMapping("/user")
public class UserController {

// 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456
@RequestMapping("/doLogin")
public String doLogin(String username, String password) {
// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
if("zhang".equals(username) && "123456".equals(password)) {
StpUtil.login(10001);
return "登录成功";
}
return "登录失败";
}

// 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin
@RequestMapping("/isLogin")
public String isLogin() {
return "当前会话是否登录:" + StpUtil.isLogin();
}

}

2 登录认证

2.1 设计思路

增加一层接口校验

  • 如果校验通过,则:正常返回数据。
  • 如果校验未通过,则:抛出异常,告知其需要先进行登录。

登录访问流程

  1. 用户提交 name + password 参数,调用登录接口。
  2. 登录成功,返回这个用户的 Token 会话凭证。
  3. 用户后续的每次请求,都携带上这个 Token。
  4. 服务器根据 Token 判断此会话是否登录成功。

2.2 登陆与注销

登录

StpUtil.login(Object id);
会话登录

参数填写要登录的账号id,建议的数据类型:long | int | String,不可以传入复杂类型,如:User、Admin 等等

Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端,利用了 Cookie 自动注入的特性,省略了手写返回 Token 的代码。

Cookie基本功能:

  • Cookie 可以从后端控制往浏览器中写入 Token 值。
  • Cookie 会在每次请求时自动提交 Token 值。

注销

StpUtil.logout();
当前会话注销登录

登陆状态

StpUtil.isLogin();
获取当前会话是否已经登录,返回true=已登录,false=未登录

StpUtil.checkLogin();
检验当前会话是否已经登录, 如果未登录,则抛出异常:NotLoginException

2.3 会话查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.getLoginId();

// 类似查询API还有:
StpUtil.getLoginIdAsString(); // 获取当前会话账号id, 并转化为`String`类型
StpUtil.getLoginIdAsInt(); // 获取当前会话账号id, 并转化为`int`类型
StpUtil.getLoginIdAsLong(); // 获取当前会话账号id, 并转化为`long`类型

// ---------- 指定未登录情形下返回的默认值 ----------

// 获取当前会话账号id, 如果未登录,则返回null
StpUtil.getLoginIdDefaultNull();

// 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)
StpUtil.getLoginId(T defaultValue);

2.4 Token查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取当前会话的token值
StpUtil.getTokenValue();

// 获取当前`StpLogic`的token名称
StpUtil.getTokenName();

// 获取指定token对应的账号id,如果未登录,则返回 null
StpUtil.getLoginIdByToken(String tokenValue);

// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
StpUtil.getTokenTimeout();

// 获取当前会话的token信息参数
StpUtil.getTokenInfo();

TokenInfo详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"code": 200,
"msg": "ok",
"data": {
"tokenName": "satoken", // token名称
"tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633", // token值
"isLogin": true, // 此token是否已经登录
"loginId": "10001", // 此token对应的LoginId,未登录时为null
"loginType": "login", // 账号类型标识
"tokenTimeout": 2591977, // token剩余有效期 (单位: 秒)
"sessionTimeout": 2591977, // User-Session剩余有效时间 (单位: 秒)
"tokenSessionTimeout": -2, // Token-Session剩余有效时间 (单位: 秒)
"tokenActivityTimeout": -1, // token剩余无操作有效时间 (单位: 秒)
"loginDevice": "default-device" // 登录设备类型
},
}

2.5 示例

新建 LoginController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 登录测试
*/
@RestController
@RequestMapping("/acc/")
public class LoginController {

// 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {
// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
if("zhang".equals(name) && "123456".equals(pwd)) {
StpUtil.login(10001);
return SaResult.ok("登录成功");
}
return SaResult.error("登录失败");
}

// 查询登录状态 ---- http://localhost:8081/acc/isLogin
@RequestMapping("isLogin")
public SaResult isLogin() {
return SaResult.ok("是否登录:" + StpUtil.isLogin());
}

// 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo
@RequestMapping("tokenInfo")
public SaResult tokenInfo() {
return SaResult.data(StpUtil.getTokenInfo());
}

// 测试注销 ---- http://localhost:8081/acc/logout
@RequestMapping("logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok();
}

}

3 权限认证

3.1 设计思路

核心逻辑就是判断一个账号是否拥有指定权限

深入到底层数据中,就是每个账号都会拥有一个权限码集合,框架来校验这个集合中是否包含指定的权限码。

例如:当前账号拥有权限码集合 ["user-add", "user-delete", "user-get"],这时候我来校验权限 "user-update",则其结果就是:验证失败,禁止访问

3.2 获取当前账号权限码集合

新建一个类,实现 StpInterface接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 自定义权限验证接口扩展
*/
@Component // 保证此类被SpringBoot扫描,完成Sa-Token的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {

/**
* 返回一个账号所拥有的权限码集合
* @param loginId 账号id,即你在调用 StpUtil.login(id) 时写入的标识值。
* @param loginType 账号体系标识
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
List<String> list = new ArrayList<String>();
list.add("101");
list.add("user-add");
list.add("user-delete");
list.add("user-update");
list.add("user-get");
list.add("article-get");
return list;
}

/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
List<String> list = new ArrayList<String>();
list.add("admin");
list.add("super-admin");
return list;
}

}

3.3 权限认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取:当前账号所拥有的权限集合
StpUtil.getPermissionList();

// 判断:当前账号是否含有指定权限, 返回true或false
StpUtil.hasPermission("user-update");

// 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException
StpUtil.checkPermission("user-update");

// 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
StpUtil.checkPermissionAnd("user-update", "user-delete");

// 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
StpUtil.checkPermissionOr("user-update", "user-delete");

扩展:NotPermissionException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常

3.4 角色认证

在Sa-Token中,角色和权限可以独立验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取:当前账号所拥有的角色集合
StpUtil.getRoleList();

// 判断:当前账号是否拥有指定角色, 返回true或false
StpUtil.hasRole("super-admin");

// 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
StpUtil.checkRole("super-admin");

// 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
StpUtil.checkRoleAnd("super-admin", "shop-admin");

// 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
StpUtil.checkRoleOr("super-admin", "shop-admin");

扩展:NotRoleException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常

3.5 拦截全局异常

创建一个全局异常拦截器,统一返回给前端的格式

1
2
3
4
5
6
7
8
9
@RestControllerAdvice
public class GlobalExceptionHandler {
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}

3.6 权限通配符

Sa-Token允许你根据通配符指定泛权限,例如当一个账号拥有user*的权限时,user-adduser-deleteuser-update都将匹配通过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 当拥有 user* 权限时
StpUtil.hasPermission("user-add"); // true
StpUtil.hasPermission("user-update"); // true
StpUtil.hasPermission("art-add"); // false

// 当拥有 *-delete 权限时
StpUtil.hasPermission("user-add"); // false
StpUtil.hasPermission("user-delete"); // true
StpUtil.hasPermission("art-delete"); // true

// 当拥有 *.js 权限时
StpUtil.hasPermission("index.js"); // true
StpUtil.hasPermission("index.css"); // false
StpUtil.hasPermission("index.html"); // false

上帝权限:当一个账号拥有 "*" 权限时,他可以验证通过任何权限码 (角色认证同理)

3.7 如何把权限精确到按钮级

权限精确到按钮级的意思就是指:权限范围可以控制到页面上的每一个按钮是否显示

  • 在登录时,把当前账号拥有的所有权限码一次性返回给前端。
  • 前端将权限码集合保存在localStorage或其它全局状态管理对象中。
  • 在需要权限控制的按钮上,使用 js 进行逻辑判断

4 踢人下线

找到指定 loginId 对应的 Token,并设置其失效。

4.1 强制注销

1
2
3
StpUtil.logout(10001);                    // 强制指定账号注销下线 
StpUtil.logout(10001, "PC"); // 强制指定账号指定端注销下线, 可在登录时设置设备
StpUtil.logoutByTokenValue("token"); // 强制指定 Token 注销下线

4.2 踢人下线

1
2
3
StpUtil.kickout(10001);                    // 将指定账号踢下线 
StpUtil.kickout(10001, "PC"); // 将指定账号指定端踢下线
StpUtil.kickoutByTokenValue("token"); // 将指定 Token 踢下线

强制注销 和 踢人下线 的区别在于:

  • 强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效。
  • 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线。

4.3 账号封禁

1
2
3
4
5
6
7
8
9
10
11
12
13
// 封禁指定账号 
// 参数一:账号id
// 参数二:封禁时长,单位:秒 (86400秒=1天,此值为-1时,代表永久封禁)
StpUtil.disable(10001, 86400);

// 获取指定账号是否已被封禁 (true=已被封禁, false=未被封禁)
StpUtil.isDisable(10001);

// 获取指定账号剩余封禁时间,单位:秒
StpUtil.getDisableTime(10001);

// 解除封禁
StpUtil.untieDisable(10001);

注意

对于正在登录的账号,对其账号封禁时并不会使其立刻注销
如果需要将其封禁后立即掉线,可采取先踢再封禁的策略。

5 注解鉴权

6 路由拦截鉴权

7 Session会话

8 框架配置