Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式 Session 会话、微服务网关鉴权等一系列权限相关问题。
学习地址:框架介绍 (sa-token.cc)
以下的各种资料均来自官网,为官网的极简使用版,如有其他问题请查阅官网。如文章与 Sa-Token 官网内容不一致以官方资料为准。
Sa-Token 官方文档写的极其优秀
SaToken 相比于其他权限认证框架,具有以下几项显著优势:
SaToken 设计为轻量级框架,减少了对应用程序的侵入性,同时优化了性能,确保了在高并发场景下的稳定运行。
SaToken 它提供了登录认证、权限认证、Session 管理、单点登录(SSO)、 OAuth2.0 支持、微服务网关鉴权等全面的权限管理功能,简化了系统权限架构的设计与实现过程。
SaToken 通过简单的 API 调用和注解即可实现复杂的权限控制逻辑,降低了代码复杂度,提高了开发效率。
SaToken 提供了便捷的集成方式,只需少量配置即可快速应用于项目中,且支持高度自定义,能满足不同项目的需求。
SaToken 适合于前后端分离的项目架构,支持跨域认证,前端可以直接通过 JWT 等机制进行无状态验证,提升了用户体验。
针对微服务架构,SaToken 提供了微服务间认证与授权的解决方案,包括分布式会话管理和微服务网关的集成。
在 Springboot 项目中, 我们通过引入 pom 文件并且添加 yml 配置的方式完成 SaToken 的集成。
首先在项目中引入依赖,若为 Springboot2 项目,则将 artifactId 改为 sa-token-spring-boot-starter。
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.38.0</version>
</dependency>
在 yml 中添加配置:
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
token-style: uuid
# 是否输出操作日志
is-log: true
Spring Boot 测试类
@RestController
public class SaTokenController {
@PostMapping("satoken/login")
public String doLogin(@RequestBody String username, String password){
StpUtil.login(username);
return "success";
}
@PostMapping("satoken/isLogin")
public String doLogout(@RequestBody String username){
boolean login = StpUtil.isLogin(username);
return login+"";
}
}
使用测试工具进行测试,SaToken 成功集成。
可以看到 SaToken 通过封装常用功能使我们仅需要通过某一个方法即可实现我们所需要的功能。
我们完成了一个最简单的登录验证 Demo,此时我们可以发现 SaToken 将用户的登录信息存在了某一个地方使我们能够进行登录信息验证,下文会展示细节此处不做补充。那么我们在登录之后如何对用户进行权限验证呢?
新建 impl 实现 StpInterface 接口并重写方法:
@Service
public class StpServiceImpl implements StpInterface {
/**
* 返回一个账号所拥有的权限码集合
* @param loginId Object类型的用户id
* @param userType String类型的用户权限
* @return
*/
@Override
public List<String> getPermissionList(Object loginId, String userType) {
// 实际情况可以采用数据库配置,此处演示不做深入
List<String> list = new ArrayList<>();
list.add("add");
list.add("update");
list.add("delete");
list.add("select");
return list;
}
/**
* 返回一个账号所拥有的角色标识集合
* @param loginId Object类型的用户id
* @param userType String类型的用户权限
* @return
*/
@Override
public List<String> getRoleList(Object loginId, String userType) {
// 应结合数据库实际参数获取角色标识集合,此处演示不做深入
if ("test".equals(loginId)) {
List<String> list = new ArrayList<>();
list.add("SysAdmin");
return list;
}
return null;
}
}
测试 Controller 中新增方法:
@PostMapping("satoken/checkPermission")
public List<String> getPermission(){
List<String> permissionList = StpUtil.getPermissionList();
return permissionList;
}
@PostMapping("satoken/checkRole")
public List<String> getRole(){
List<String> roleList = StpUtil.getRoleList();
return roleList;
}
启动项目,可以看到我们登录后访问权限接口,成功拿到了权限的返回值(实际情况采用 Id 进行配置,权限不写死)。
若我们访问角色标识接口,当登录用户为 test 时,即为系统管理员 SysAdmin(此处仍应从数据库中读取数据)。
在 SaToken 的设计下,权限与角色可以分开校验。更多方法请查阅源码或官方文档。
当用户登录验证失败时,我们需要处理项目抛出的异常,SaToken 提供了全局异常拦截,当然我们也可以不采用 SaToken 提供的模板自行创建全局异常。
新建全局异常处理:
@RestControllerAdvice
public class GlobalSaTokenExceptionHandler {
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
修改登录接口用于验证:
@PostMapping("satoken/login")
public String doLogin(@RequestBody String username, String password) throws Exception {
StpUtil.login(username);
if (password == null) {
throw new Exception("密码不能为空");
}
return "success";
}
启动项目进行测试,可以看到 SaToken 返回了一个通用的 ResultVO 形式的错误处理(实际上我们也可以使用自定义的 ResultVO)。
SaToken 在设计权限内容时,考虑到了通配符的泛用权限,当我们的权限名称为 XX.XX 时,可以使用权限通配符。
// 当拥有 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
根据官网所提供的关于通配符的内容,当某一个账号的权限为 * 时,它可以通过任意的权限验证。
我们访问一些需要权限验证的接口时,不可能在每个接口中都添加权限验证的相关代码,SaToken 提供了相关注解使用。
@SaCheckLogin
: 登录校验 —— 只有登录之后才能进入该方法。@SaCheckRole("admin")
: 角色校验 —— 必须具有指定角色标识才能进入该方法。@SaCheckPermission("user:add")
: 权限校验 —— 必须具有指定权限才能进入该方法。@SaCheckSafe
: 二级认证校验 —— 必须二级认证之后才能进入该方法。@SaCheckHttpBasic
: HttpBasic校验 —— 只有通过 HttpBasic 认证后才能进入该方法。@SaCheckHttpDigest
: HttpDigest校验 —— 只有通过 HttpDigest 认证后才能进入该方法。@SaIgnore
:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。@SaCheckDisable("comment")
:账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。SaToken 使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态因此,为了使用注解鉴权,需要打开 SaToken 的拦截器。
新建 SaTokenConfig:
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}
当我们在使用项目时,常常会出现同一个接口不同权限的人都可以访问,那么此时应该如何使用权限验证呢?
// 注解式鉴权:只要具有其中一个权限即可通过校验
@RequestMapping("atJurOr")
@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR)
public SaResult atJurOr() {
return SaResult.data("用户信息");
}
SaToken 提供了 @SaCheckRole 和 @SaCheckPermission 分别对权限和角色进行校验,mode 存在两种取值,AND 表示必须全部具备才可访问,OR 表示具备其一即可访问。
如果存在具有某个权限或具有某个角色可以请求,就需要使用新的写法,该写法同样支持大括号的数组形式:
// 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验
@RequestMapping("userAdd")
@SaCheckPermission(value = "user.add", orRole = "admin")
public SaResult userAdd() {
return SaResult.data("用户信息");
}
当我们希望某一些接口不需要验证就可访问时,需要添加 SaIgnore 注解:
@SaCheckLogin
@RestController
public class TestController {
// ... 其它方法
// 此接口加上了 @SaIgnore 可以游客访问
@SaIgnore
@RequestMapping("getList")
public SaResult getList() {
// ...
return SaResult.ok();
}
}
对于 @SaIgnore:
// 在 `@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();
}
没有 @SaCheckAnd,因为权限注解单独写即表示 And 的情况。
同样的,注解也支持大括号的数组形式。
@SaCheckOr(
login = { @SaCheckLogin(type = "login"), @SaCheckLogin(type = "user") }
)
需要注意的是,如果使用拦截器模式,SaToken 仅能在 Controller 层实现注解鉴权。如需在其他层级使用注解鉴权,需要新增依赖,采用鉴权的 AOP 模式。
<!-- Sa-Token 整合 SpringAOP 实现注解鉴权 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-aop</artifactId>
<version>1.38.0</version>
</dependency>
注意:AOP 模式和拦截器模式不能同时存在,否则会出现同一校验执行两次的情况。
功能单一且重复,需要使用查询官网。
适用场景:项目中所有接口均需要登录认证,只有 “登录接口” 本身对外开放。
SaToken 所有可以配置的参数内容,请参考官网结合需求进行配置。
前文我们提到过,SaToken 通过一个简单的方法调用实现了用户的登录信息存储(见登录与权限验证小节第一段),那么 SaToken 是如何实现的?
SaToken 在方法中实现的内容有:
Token
凭证与 Session
会话;Token
注入到请求上下文;SaToken 将数据保存在内存中, 在速度极快的情况下,避免了序列化的相关问题与性能消耗。但在内存中存储数据会有以下的问题:
重启丢数据、分布式环境数据无法共享
所以,SaToken 在开发时提供了扩展接口,使得开发者可以将数据存储在一些中间件中,做到重启数据不丢失且保证分布式环境下多节点会话一致性。
尽量保证 SaToken 的 Redis 集成版本与 SaToken 引入版本一致,否则容易出现兼容性问题。
我们使用 Redis 存储用户登录信息:
<!-- Sa-Token 整合 Redis (使用 jdk 默认序列化方式) -->
<!-- 优点:兼容性好,缺点:Session 序列化后基本不可读,对开发者来讲等同于乱码 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis</artifactId>
<version>1.38.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
或使用另一种依赖:
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<!-- 优点:Session 序列化后可读性强,可灵活手动修改,缺点:兼容性稍差 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.38.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
application.yml
spring:
# redis配置
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
# password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
Redis 集成完毕后,不需要我们手动操作代码,SaToken 会自行的将数据存入 Redis。
一般的常规 Web 鉴权模式,由 Cookie 模式完成,而 Cookie 模式有两个特点:
但前后端分离的无 Cookie 模式,就无法完成默认的鉴权,所以我们需要进行处理。
// 登录接口
@RequestMapping("doLogin")
public SaResult doLogin() {
// 第1步,先登录上
StpUtil.login(10001);
// 第2步,获取 Token 相关参数
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
// 第3步,返回给前端
return SaResult.data(tokenInfo);
}
// 1、首先在登录时,将tokenName和tokenValue一起存储在本地,例如:
uni.setStorageSync('tokenName', tokenName);
uni.setStorageSync('tokenValue', tokenValue);
// 2、在发起ajax的地方,获取这两个值, 并组织到head里
var tokenName = uni.getStorageSync('tokenName'); // 从本地缓存读取tokenName值
var tokenValue = uni.getStorageSync('tokenValue'); // 从本地缓存读取tokenValue值
var header = {
"content-type": "application/x-www-form-urlencoded"
};
if (tokenName != undefined && tokenName != '') {
header[tokenName] = tokenValue;
}
// 3、后续在发起请求时将 header 对象塞到请求头部
uni.request({
url: 'https://www.example.com/request', // 仅为示例,并非真实接口地址。
header: header,
success: (res) => {
console.log(res.data);
}
});
// 示例1:
// 指定token有效期(单位: 秒),如下所示token七天有效
StpUtil.login(10001, new SaLoginModel().setTimeout(60 * 60 * 24 * 7));
// ----------------------- 示例2:所有参数
// `SaLoginModel`为登录参数Model,其有诸多参数决定登录时的各种逻辑,例如:
StpUtil.login(10001, new SaLoginModel()
.setDevice("PC") // 此次登录的客户端设备类型, 用于[同端互斥登录]时指定此次登录的设备类型
.setIsLastingCookie(true) // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
.setTimeout(60 * 60 * 24 * 7) // 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的 timeout 值)
.setToken("xxxx-xxxx-xxxx-xxxx") // 预定此次登录的生成的Token
.setIsWriteHeader(false) // 是否在登录后将 Token 写入到响应头
);
假设我们的系统有多个部分,每一个部分都需要登录一次的话,会极大的影响到用户的使用体验。为了优化这个问题,系统必须具备将这些系统的认证授权相互共享,在一个系统登录之后即可访问其他系统。
解决这个问题的方式就是单点登录。
SaToken 的单点登录分为三种模式:
前端同域+后端同 Redis(基本不符合场景)
前端不同域+后端同 Redis(适用场景不多,生产几乎不会同 Redis)
前端不同域+后端不同 Redis(生产场景)
具体如何实现查看官网为三种方式所配置的文档说明:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。