Zer0e's Blog

【架构之路11】sa-token框架上手体验

字数统计: 2k阅读时长: 8 min
2024/07/26 Share

前言

看见了一个权限框架Sa-Token,以快速上手,轻量为优点,快速完成登录认证,权限认证等功能。

这篇文章来上手体验下这个框架,并讲讲分布式情况下如何使用。

正文

创建项目

创建一个springboot3项目吧。

pom文件如下

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 这里需要使用springboot3的依赖-->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.38.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

我们用0配置来编写这个项目

登录认证

编写对应的service和controller

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class UserService {

public String doLogin(String username, String password) {
if ("zer0e".equals(username)) {
// 这里是用户id
StpUtil.login(1);
}else {
StpUtil.login(2);
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class UserController {

@Resource
private UserService userService;

@GetMapping("/login")
public String doLogin(@RequestParam String username,
@RequestParam String password) {
return userService.doLogin(username, password);
}


@GetMapping("/info")
public Object info() {
return StpUtil.getTokenInfo();
}
}

这里由于没有接入数据库,所以service中没有校验用户名密码,问题不大。

login接口使用get是为了演示。

此时先访问http://127.0.0.1:8080/login?username=zer0e&password=zer0e

然后访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
http://127.0.0.1:8080/info
{
"tokenName": "satoken",
"tokenValue": "deb45ea8-3498-4837-88bf-9dba5e2e6b7e",
"isLogin": true,
"loginId": "1",
"loginType": "login",
"tokenTimeout": 2591993,
"sessionTimeout": 2591993,
"tokenSessionTimeout": -2,
"tokenActiveTimeout": -1,
"loginDevice": "default-device",
"tag": null
}

可以发现正常登录,并且获取登录信息也是正常的。

权限和角色

光登录还不行,像spring security框架我们可以在登录时获取角色和权限以达到检验的目的。

sa-token也可以做到。

只要我们注册一个实现StpInterface接口的bean 再加上一点配置即可!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class StpInterfaceImpl implements StpInterface {

@Override
public List<String> getPermissionList(Object loginId, String loginType) {
System.out.println("获取权限列表:" + loginId);
if (loginId != null && loginId.equals(1)) {
return Arrays.asList("read", "write");
}
return List.of("read");
}

@Override
public List<String> getRoleList(Object loginId, String loginType) {
System.out.println("获取角色列表:" + loginId);
if (loginId != null && loginId.equals(1)) {
return List.of("admin");
}
return List.of("user");
}
}
1
2
3
4
5
6
7
8
9
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}

我们把SaInterceptor注册到所有路由上。

这里为了方便都是采用硬编码,实际情况下是去数据库查询,这里不再赘述。

增加三个权限相关的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@GetMapping("/admin")
@SaCheckRole("admin")
public Boolean admin() {
return true;
}

@GetMapping("/read")
@SaCheckPermission("read")
public Boolean read() {
return true;
}

@GetMapping("/write")
@SaCheckPermission("write")
public Boolean write() {
return true;
}

先登录zer0e,然后访问/admin /write /read均可以访问。

然后登录其他用户,发现除了/read,其他接口都是500。因为我们没有做全局异常处理,所以抛出了500异常,问题不大。

如果我们需要在service层做权限校验,那么我们必须引入aop依赖

1
2
3
4
5
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-aop</artifactId>
<version>1.38.0</version>
</dependency>

分布式环境

默认情况下,sa框架使用内存进行token存储,对于分布式环境下不太友好,好在sa提供了redis集成,我们加上即可。

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis</artifactId>
<version>1.38.0</version>
</dependency>
<!-- 也可以选择jackson序列化
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.38.0</version>
</dependency>
--!>

由于我们创建项目时就引入了redis,因此我们这里直接进行配置即可。

1
2
3
4
5
6
7
spring:
application:
name: sa-token-demo
data:
redis:
host: localhost
port: 6379

基本上不用动东西,此时再次登录后发现token会存储在redis中,重启项目后原先token也可以使用。

角色权限缓存

从刚才的权限例子我们可以知道,每次访问接口时,sa框架都会去获取用户id所对应的角色或权限(取决于你做了什么校验)。这对数据库的压力是比较大的,因此必须做缓存。

看了官方文档,其实是可以把一部分数据一起缓存的。这里我直接上代码。

1
2
3
4
5
6
7
8
9
10
11
@Override
public List<String> getRoleList(Object loginId, String loginType) {
SaSession session = StpUtil.getSessionByLoginId(loginId);
return session.get("roles", () -> {
System.out.println("从数据库获取角色列表:" + loginId);
if (loginId != null && loginId.equals(1)) {
return List.of("admin");
}
return List.of("user");
});
}

SaSession对象其实就是一个缓存,我们不必自己对接redisTemplate。至于权限也是一样的,但是这里官方做了一个解释,大概意思就是不要直接缓存账号的权限,而是通过获取角色再获取权限,我觉得说的也没毛病,因为你一旦角色和权限的对应关系更改后,权限缓存需要大面积失效,而且还不知道要失效哪些。

疑问:为什么不直接缓存 [账号id->权限列表\]的关系,而是 [账号id -> 角色id -> 权限列表]

答:[账号id->权限列表]的缓存方式虽然更加直接粗暴,却有一个严重的问题:

  • 通常我们系统的权限架构是RBAC模型:权限与用户没有直接的关系,而是:用户拥有指定的角色,角色再拥有指定的权限
  • 而这种’拥有关系’是动态的,是可以随时修改的,一旦我们修改了它们的对应关系,便要同步修改或清除对应的缓存数据

现在假设如下业务场景:我们系统中有十万个账号属于同一个角色,当我们变动这个角色的权限时,难道我们要同时清除这十万个账号的缓存信息吗? 这显然是一个不合理的操作,同一时间缓存大量清除容易引起Redis的缓存雪崩

而当我们采用 [账号id -> 角色id -> 权限列表] 的缓存模型时,则只需要清除或修改 [角色id -> 权限列表] 一条缓存即可

一言以蔽之:权限的缓存模型需要跟着权限模型走,角色缓存亦然

因此官网的做法是获取用户的角色,然后以角色id为key获取权限缓存,这样权限有更改时,我们只要失效对应的角色id的key即可,即roleSession.clear()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public List<String> getPermissionList(Object loginId, String loginType) {

// 1. 声明权限码集合
List<String> permissionList = new ArrayList<>();

// 2. 遍历角色列表,查询拥有的权限码
for (String roleId : getRoleList(loginId, loginType)) {
SaSession roleSession = SaSessionCustomUtil.getSessionById("role-" + roleId);
List<String> list = roleSession.get("Permission_List", () -> {
return ...; // 从数据库查询这个角色所拥有的权限列表
});
permissionList.addAll(list);
}

// 3. 返回权限码集合
return permissionList;
}

总结

sa-token相对于spring security轻量了不少,使用上很简单,如果要配置权限校验的路径的话可以在new SaInterceptor中来指定。

此外它还有很多功能,比如踢人下线,多端登录等功能。甚至可以完全放弃session模式,改用jwt去存储token。

从今天的学习可以发现入门真的很快。如果要快速开发,那么sa-token是一个不错的选择。

原文作者:Zer0e

原文链接:https://re0.top/2024/07/26/devops11/

发表日期:July 26th 2024, 2:30:00 pm

更新日期:July 26th 2024, 2:34:07 pm

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. 前言
  2. 2. 正文
    1. 2.1. 创建项目
    2. 2.2. 登录认证
    3. 2.3. 权限和角色
    4. 2.4. 分布式环境
    5. 2.5. 角色权限缓存
      1. 2.5.0.0.1. 疑问:为什么不直接缓存 [账号id->权限列表\]的关系,而是 [账号id -> 角色id -> 权限列表]?
  • 3. 总结