前言
看见了一个权限框架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> <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)) { 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) { 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>
|
由于我们创建项目时就引入了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) { List<String> permissionList = new ArrayList<>(); for (String roleId : getRoleList(loginId, loginType)) { SaSession roleSession = SaSessionCustomUtil.getSessionById("role-" + roleId); List<String> list = roleSession.get("Permission_List", () -> { return ...; }); permissionList.addAll(list); } return permissionList; }
|
总结
sa-token相对于spring security轻量了不少,使用上很简单,如果要配置权限校验的路径的话可以在new SaInterceptor
中来指定。
此外它还有很多功能,比如踢人下线,多端登录等功能。甚至可以完全放弃session模式,改用jwt去存储token。
从今天的学习可以发现入门真的很快。如果要快速开发,那么sa-token是一个不错的选择。