Sa-Token介绍与SpringBoot环境下使用

个人博客:无奈何杨(wnhyang)

个人语雀:wnhyang

共享语雀:在线知识共享

Github:wnhyang - Overview


官网:Sa-Token

一个轻量级 Java 权限认证框架,让鉴权变得简单、优雅!

介绍

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0分布式Session会话微服务网关鉴权 等一系列权限相关问题。

官方文档写的已经非常好了。引用官方文档开头的一段话:

本文档将会尽力讲解每个功能的设计原因、应用场景,用心阅读文档,你学习到的将不止是 Sa-Token 框架本身,更是绝大多数场景下权限设计的最佳实践。

确实,通过阅读官方文档有学到很多东西,收获更大的是结合我的使用体验,下载并阅读源码后有学到了一些东西想和大家分享!这篇文章只是开头。

使用

1、准备工作

环境:

Spring Boot2.7.3

Sa-Token1.37.0

2、配置文件

关于sa-token的配置文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: Authorization
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 3600
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: 1800
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: false
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: true
# 是否尝试从header里读取token
is-read-header: true
# 是否尝试从cookie里读取token
is-read-cookie: false
# token前缀
token-prefix: "Bearer"
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
token-style: tik
# 是否输出操作日志
is-log: true

3、自定义权限认证

官方文档:因为每个项目的需求不同,其权限设计也千变万化,因此 [ 获取当前账号权限码集合 ] 这一操作不可能内置到框架中, 所以 Sa-Token 将此操作以接口的方式暴露给你,以方便你根据自己的业务逻辑进行重写。

所以根据自己的不同业务设计需要的权限认证方法就好。下面是我项目中的写法,仅供参考。

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
@Setter
public class StpInterfaceImpl implements StpInterface {

/**
* 登录服务
*/
private LoginService loginService;

/**
* 返回指定账号id所拥有的权限码集合
*
* @param loginId 账号id
* @param loginType 账号类型
* @return 该账号id具有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
Login loginUser = loginService.getLoginUser();
UserTypeEnum userType = UserTypeEnum.valueOf(loginUser.getType());
if (userType == UserTypeEnum.PC) {
return new ArrayList<>(loginUser.getPermissions());
}

return new ArrayList<>();
}

/**
* 返回指定账号id所拥有的角色标识集合
*
* @param loginId 账号id
* @param loginType 账号类型
* @return 该账号id具有的角色标识集合
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
Login loginUser = loginService.getLoginUser();
UserTypeEnum userType = UserTypeEnum.valueOf(loginUser.getType());
if (userType == UserTypeEnum.PC) {
return new ArrayList<>(loginUser.getRoleValues());
}

return new ArrayList<>();
}
}

接着注册StpInterfaceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class SaTokenConfiguration {

@Bean
public LoginService loginService() {
return new LoginServiceImpl();
}

@Bean
public StpInterface stpInterface(LoginService loginService) {
StpInterfaceImpl stpInterface = new StpInterfaceImpl();
stpInterface.setLoginService(loginService);
return stpInterface;
}
}

4、Controller

4.1、工具类鉴权

satoken提供了StpUtill鉴权工具类,其中包含非常多的静态方法可以使用,详情请参考:Sa-Token

1
2
3
4
5
6
// 会话登录,参数填登录人的账号id 
StpUtil.login(10001);
// 校验当前客户端是否已经登录,如果未登录则抛出 `NotLoginException` 异常
StpUtil.checkLogin();
// 将账号id为 10077 的会话踢下线
StpUtil.kickout(10077);

4.2、注解鉴权

尽管StpUtill鉴权工具类已经非常方便但还是有同学钟爱注解鉴权,satoken也是提供了这种方式的。

1
2
3
4
5
6
// 注解鉴权:只有具备 `user:add` 权限的会话才可以进入方法
@SaCheckPermission("user:add")
public String insert(SysUser user) {
// ...
return "用户增加";
}

注解鉴权默认是关闭的,要使用的话需要将satoken全局拦截器注入到项目中!

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class SecurityConfiguration implements WebMvcConfigurer {

/**
* 注册sa-token的拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册路由拦截器,自定义验证规则
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}

如上就注册了satoken的全局拦截器,就可以愉快的使用注解权限了。官方文档:Sa-Token

注意:这里配置的是“/**”,也就是全路径,所有后面要在不需要鉴权的接口上加上@SaIgnore用于忽略鉴权。

如下配置了/auth开头的接口不用鉴权

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

private final AuthService authService;

/**
* 登录
*
* @param reqVO 登录请求
* @return token
*/
@PostMapping("/login")
public CommonResult<LoginRespVO> login(@RequestBody @Valid LoginReqVO reqVO) {
return success(authService.login(reqVO));
}

...
}

如下是一个简单的需要鉴权的示例

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
@RestController
@RequestMapping("/system/role")
@RequiredArgsConstructor
public class RoleController {

private final RoleService roleService;

/**
* 创建角色
*
* @param reqVO 角色信息
* @return id
*/
@PostMapping("/create")
@SaCheckPermission("system:role:create")
public CommonResult<Long> createRole(@Valid @RequestBody RoleCreateReqVO reqVO) {
return success(roleService.createRole(reqVO));
}

/**
* 更新角色
*
* @param reqVO 角色信息
* @return 结果
*/
@PutMapping("/update")
@SaCheckPermission("system:role:update")
public CommonResult<Boolean> updateRole(@Valid @RequestBody RoleUpdateReqVO reqVO) {
roleService.updateRole(reqVO);
return success(true);
}

...
}

5、全局异常

通过定义全局异常拦截器可以返回前端统一的格式,以下仅供参考。

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
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 权限码异常
*/
@ExceptionHandler(NotPermissionException.class)
public CommonResult<Void> handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
String requestUri = request.getRequestURI();
log.error("请求地址'{}',权限码校验失败'{}'", requestUri, e.getMessage());
return CommonResult.error(FORBIDDEN);
}

/**
* 角色权限异常
*/
@ExceptionHandler(NotRoleException.class)
public CommonResult<Void> handleNotRoleException(NotRoleException e, HttpServletRequest request) {
String requestUri = request.getRequestURI();
log.error("请求地址'{}',角色权限校验失败'{}'", requestUri, e.getMessage());
return CommonResult.error(FORBIDDEN);
}

/**
* 认证失败
*/
@ExceptionHandler(NotLoginException.class)
public CommonResult<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
String requestUri = request.getRequestURI();
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestUri, e.getMessage());
return CommonResult.error(UNAUTHORIZED);
}

/**
* 无效认证
*/
@ExceptionHandler(SameTokenInvalidException.class)
public CommonResult<Void> handleSameTokenInvalidException(SameTokenInvalidException e, HttpServletRequest request) {
String requestUri = request.getRequestURI();
log.error("请求地址'{}',内网认证失败'{}',无法访问系统资源", requestUri, e.getMessage());
return CommonResult.error(UNAUTHORIZED);
}

...

}

6、跨域(可选)

官网也有提供解决方法Sa-Token,我非常建议你看完这篇文章后再决定使用什么方式Sa-Token

组建简述

看个人选择吧,我这里使用的是satoken拦截器+普通corsFilter的方式。

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
@Configuration
public class OkayWebAutoConfiguration implements WebMvcConfigurer {

/**
* 跨域配置
*/
@Bean
public CorsFilter corsFilter()
{
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 设置访问源地址
config.addAllowedOriginPattern("*");
// 设置访问源请求头
config.addAllowedHeader("*");
// 设置访问源请求方法
config.addAllowedMethod("*");
// 有效期 1800秒
config.setMaxAge(1800L);
// 添加映射路径,拦截一切请求
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
// 返回新的CorsFilter
return new CorsFilter(source);
}

}

7、启动类

启动类确保以上需要的组件被扫描注册就好。

1
2
3
4
5
6
7
@SpringBootApplication
public class AdminApplication {

public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}

8、启动测试

这里我使用了redis,可以先忽略。

正常启动

因为我的前端是一整个项目,不好拆出来贴代码,所以直接展示登录结果了!

尝试登录

成功登录

发起一个创建请求,因为前端项目已经配置了发请求的header,所以请求就会带上前面的token了。

请求

然后satoken拦截器发挥作用进行鉴权,关于拦截器如何鉴权的,我之后还会再讲的。这次先到这吧。


个人博客:无奈何杨(wnhyang)

个人语雀:wnhyang

共享语雀:在线知识共享

Github:wnhyang - Overview