目录
一、简介与配置
二、实现登录注销操作
三、权限认证构成与鉴权方法 <— 你在这里 ( •̀ ω •́ )y
四、RBAC 结构实现

Sa-Token(三)权限认证构成与鉴权方法

[!NOTE]

博主前言:Spring Security 的学习真是惊掉我下巴,就一个鉴权功能,搞那么多复杂概念,又是过滤器链又是注入的,花了我一个星期左右,实际上还很不好用(未知错误给前端返回 401 是真绷不住)。Sa-Token 作为国产鉴权,性能更强的同时简化开发的程度相比 Spring Security 起码有十倍不止。最近我还注意到对标 Spring 生态的 Solon 框架,也是国产,性能提升3倍多,内存占用减少 50%+,打包还更小,真不知道这些工作为什么老外都做的这么复杂。

有了登录,接下来就要对接口配置拦截,进一步地还能设置权限。

在这之前,我们先来了解一下 Sa-Token 权限认证的构造。


一、权限类型简介

Sa-Token 的权限是经典的RBAC结构,分为两种类型:

  • 权限码:具体的某种权限,如新增修改
  • 角色:用户的抽象概念,或者说是权限码的集合,如管理员拥有所有权限普通用户只拥有浏览和提交权限等

[!IMPORTANT]

实际上,Sa-Token 中的角色与权限码并没有强关联,为用户注入权限时,需要手动将二者均注入一遍

[!TIP]

关于什么是RBAC结构,参考这篇文章:Spring Security(六)RBAC 结构实现

对于权限码:为程序预先制定,可定义为常量调用。

对于角色:可由指定权限的用户添加/修改,则需定义数据表存储


二、三种鉴权方法

先了解鉴权方法是如何调用的,之后再实际上手。

[!IMPORTANT]

一般情况下,验证不通过时会抛出NotPermissionException(权限码)和NotRoleException(角色)这两种异常。

  1. 静态方法鉴权:

调用StpUtil的静态方法即可实现鉴权,可以调用鉴权失败时返回false而不抛出异常的方法。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* 权限码 */

// 获取:当前账号所拥有的权限集合
StpUtil.getPermissionList();

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

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

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

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

/* 角色 */

// 获取:当前账号所拥有的角色集合
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");

/*
* Sa-Token允许你根据通配符指定泛权限
* 例如当一个账号拥有art.*的权限时
* art.add、art.delete、art.update都将匹配通过
*/
// 当拥有 art.* 权限时
StpUtil.hasPermission("art.add"); // true
StpUtil.hasPermission("art.update"); // true
StpUtil.hasPermission("goods.add"); // false

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

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

  1. 注解鉴权:

我们可以使用注解在方法上标注,没有权限的用户调用该方法会抛出对应的权限异常。

[!WARNING]

不建议在Controller层以外的方法使用注解鉴权,虽然 Sa-Token 提供了额外包实现其他位置的注解鉴权,但这样做会徒增设计复杂度,后续难以维护。

首先,在Web层配置类注册 Sa-Token 拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 注册 web 层相关组件
*
* @author Amane64
*/
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

/**
* 注册 Sa-Token 拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
log.info("注册 Sa-Token 拦截器");
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}

}

然后就可以使用注解鉴权了,使用例:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// 登录校验:只有登录之后才能进入该方法 
@SaCheckLogin
@RequestMapping("info")
public String info() {
return "查询用户信息";
}

// 角色校验:必须具有指定角色才能进入该方法
@SaCheckRole("super-admin")
@RequestMapping("add")
public String add() {
return "用户增加";
}

// 权限校验:必须具有指定权限才能进入该方法
@SaCheckPermission("user-add")
@RequestMapping("add")
public String add() {
return "用户增加";
}

// 二级认证校验:必须二级认证之后才能进入该方法
@SaCheckSafe()
@RequestMapping("add")
public String add() {
return "用户增加";
}

// Http Basic 校验:只有通过 Http Basic 认证后才能进入该方法
@SaCheckHttpBasic(account = "sa:123456")
@RequestMapping("add")
public String add() {
return "用户增加";
}

// Http Digest 校验:只有通过 Http Digest 认证后才能进入该方法
@SaCheckHttpDigest(value = "sa:123456")
@RequestMapping("add")
public String add() {
return "用户增加";
}

// 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法
@SaCheckDisable("comment")
@RequestMapping("send")
public String send() {
return "查询用户信息";
}

// @SaCheckRole与@SaCheckPermission注解可设置校验模式
// mode有两种取值:
// SaMode.AND,标注一组权限,会话必须全部具有才可通过校验。
// SaMode.OR,标注一组权限,会话只要具有其一即可通过校验。
// 注解式鉴权:只要具有其中一个权限即可通过校验
@RequestMapping("atJurOr")
@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR)
public SaResult atJurOr() {
return SaResult.data("用户信息");
}

// 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验
@RequestMapping("userAdd")
// 需要角色 admin
@SaCheckPermission(value = "user.add", orRole = "admin")
// 需要三个角色其一即可
@SaCheckPermission(value = "user.add", orRole = {"admin", "manager", "staff"})
// 需要同时具有三个角色
@SaCheckPermission(value = "user.add", orRole = {"admin, manager, staff"})
public SaResult userAdd() {
return SaResult.data("用户信息");
}

// 使用 @SaIgnore 可表示一个接口忽略认证
// @SaIgnore 同样可以忽略掉 Sa-Token 拦截器中的路由鉴权
@SaIgnore
@RequestMapping("getList")
public SaResult getList() {
// ...
return SaResult.ok();
}

// 在 `@SaCheckOr` 中可以指定多个注解,只要当前会话满足其中一个注解即可通过验证,进入方法。
@SaCheckOr(
login = @SaCheckLogin,
role = @SaCheckRole("admin"),
permission = @SaCheckPermission("user.add"),
safe = @SaCheckSafe("update-password"),
httpBasic = @SaCheckHttpBasic(account = "sa:123456"),
disable = @SaCheckDisable("submit-orders")
)
@RequestMapping("test")
public SaResult test() {
// ...
return SaResult.ok();
}

// 当前客户端只要有 [ login 账号登录] 或者 [user 账号登录] 其一,就可以通过验证进入方法。
// 注意:`type = "login"` 和 `type = "user"` 是多账号模式章节的扩展属性,此处你可以先略过这个知识点。
@SaCheckOr(
login = { @SaCheckLogin(type = "login"), @SaCheckLogin(type = "user") }
)
@RequestMapping("test")
public SaResult test() {
// ...
return SaResult.ok();
}

// 当你在一个方法上写多个注解鉴权时,其默认就是要满足所有注解规则后,才可以进入方法,只要有一个不满足,就会抛出异常
@SaCheckLogin
@SaCheckRole("admin")
@SaCheckPermission("user.add")
@RequestMapping("test")
public SaResult test() {
// ...
return SaResult.ok();
}
  1. 路由拦截鉴权:

实际开发中,我们常有登录拦截相关的需求,即项目中所有接口均需要登录认证,只有 “登录接口” 本身对外开放,在这个需求中我们真正需要的是一种基于路由拦截的鉴权模式。

我们在注解鉴权中注册了 Sa-Token 拦截器,而路由拦截鉴权正是在该拦截器上进行编辑配置的:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 注册 Sa-Token 拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
log.info("注册 Sa-Token 拦截器");
registry.addInterceptor(
// 绑定 checkLogin 方法,对路由进行登录验证
new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**")
// 放行登录路由接口
.excludePathPatterns("/login");
}

如此,即配置了最基础的登录路由拦截,除登录接口以外均需要登录才可访问。

读者亦可采用更复杂的配置,以此实现一些特殊的逻辑功能(如多端多权):

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
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册 Sa-Token 的拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册路由拦截器,自定义认证规则
registry.addInterceptor(new SaInterceptor(handler -> {

// 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin());

// 角色校验 -- 拦截以 admin 开头的路由,必须具备 admin 角色或者 super-admin 角色才可以通过认证
SaRouter.match("/admin/**", r -> StpUtil.checkRoleOr("admin", "super-admin"));

// 权限校验 -- 不同模块校验不同权限
SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment"));

// 甚至你可以随意的写一个打印语句
SaRouter.match("/**", r -> System.out.println("----啦啦啦----"));

// 连缀写法
SaRouter.match("/**").check(r -> System.out.println("----啦啦啦----"));

})).addPathPatterns("/**");
}
}