简介
Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0、微服务网关鉴权 等一系列权限相关问题
功能大全
StpUtil.login(1); // 标记当前会话登录的账号id
StpUtil.getLoginId(); // 获取当前会话登录的账号id
StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false
StpUtil.logout(); // 当前会话注销登录
StpUtil.kickout(1); // 将账号id为1的会话踢下线
StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false
StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false
StpUtil.getSession(); // 获取当前账号id的Session
StpUtil.getSessionByLoginId(1); // 获取账号id为1的Session
StpUtil.getTokenValueByLoginId(1); // 获取账号id为1的token令牌值
StpUtil.login(1, "PC"); // 指定设备标识登录,常用于“同端互斥登录”
StpUtil.kickout(1, "PC"); // 指定账号指定设备标识踢下线 (不同端不受影响)
StpUtil.openSafe(120); // 在当前会话开启二级认证,有效期为120秒
StpUtil.checkSafe(); // 校验当前会话是否处于二级认证有效期内,校验失败会抛出异常
StpUtil.switchTo(2); // 将当前会话身份临时切换为其它账号
- 登录认证 —— 单端登录、多端登录、同端互斥登录、七天内免登录
- 权限认证 —— 权限认证、角色认证、会话二级认证
- Session会话 —— 全端共享Session、单端独享Session、自定义Session
- 踢人下线 —— 根据账号id踢人下线、根据Token值踢人下线
- 账号封禁 —— 指定天数封禁、永久封禁、设定解封时间
- 持久层扩展 —— 可集成Redis、Memcached等专业缓存中间件,重启数据不丢失
- 分布式会话 —— 提供jwt集成、共享数据中心两种分布式会话方案
- 微服务网关鉴权 —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证
- 单点登录 —— 内置三种单点登录模式:无论是否跨域、是否共享Redis,都可以搞定
- OAuth2.0认证 —— 基于RFC-6749标准编写,OAuth2.0标准流程的授权认证,支持openid模式
- 二级认证 —— 在已登录的基础上再次认证,保证安全性
- Basic认证 —— 一行代码接入 Http Basic 认证
- 独立Redis —— 将权限缓存与业务缓存分离
- 临时Token验证 —— 解决短时间的Token授权问题
- 模拟他人账号 —— 实时操作任意用户状态数据
- 临时身份切换 —— 将会话身份临时切换为其它账号
- 前后台分离 —— APP、小程序等不支持Cookie的终端
- 同端互斥登录 —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录
- 多账号认证体系 —— 比如一个商城项目的user表和admin表分开鉴权
- 花式token生成 —— 内置六种Token风格,还可:自定义Token生成策略、自定义Token前缀
- 注解式鉴权 —— 优雅的将鉴权与业务代码分离
- 路由拦截式鉴权 —— 根据路由拦截鉴权,可适配restful模式
- 自动续签 —— 提供两种Token过期策略,灵活搭配使用,还可自动续签
- 会话治理 —— 提供方便灵活的会话查询接口
- 记住我模式 —— 适配[记住我]模式,重启浏览器免验证
- 密码加密 —— 提供密码加密模块,可快速MD5、SHA1、SHA256、AES、RSA加密
- 全局侦听器 —— 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作
- 开箱即用 —— 提供SpringMVC、WebFlux等常见web框架starter集成包,真正的开箱即用
使用---基础
1、引入依赖 (第一步)
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.39.0</version>
</dependency>
2、在application.yml中配置 (第二步)
你可以零配置启动项目 ,但同时你也可以在 application.yml 中增加如下配置,定制性使用框架
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
3、登录认证
所谓登录认证,指的就是服务器校验账号密码,为用户颁发 Token 会话凭证的过程,这个 Token 也是我们后续判断会话是否登录的关键所在
3.1、登录与注销 (第三步)
StpUtil.login(Object); //会话登录
只此一句代码,便可以使会话登录成功,实际上,Sa-Token 在背后做了大量的工作,包括但不限于:
- 检查此账号是否之前已有登录
- 为账号生成 Token 凭证与 Session 会话
- 记录 Token 活跃时间
- 通知全局侦听器,xx 账号登录成功
- 将 Token 注入到请求上下文
你只需要记住关键一点:Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端
StpUtil.logout(); //当前会话注销登录
StpUtil.isLogin(); //获取当前会话是否已经登录,返回true=已登录,false=未登录
StpUtil.checkLogin(); //检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
NotLoginException可能的原因:
3.2、会话查询
StpUtil.getLoginId(); //获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.getLoginIdAsString(); //获取当前会话账号id, 并转化为`String`类型
StpUtil.getLoginIdAsInt(); //获取当前会话账号id, 并转化为`int`类型
StpUtil.getLoginIdAsLong(); //获取当前会话账号id, 并转化为`long`类型
StpUtil.getLoginIdDefaultNull(); //获取当前会话账号id, 如果未登录,则返回 null
StpUtil.getLoginId(T defaultValue); //获取当前会话账号id, 如果未登录,则返回默认值(`defaultValue`可以为任意类型)
3.3、token 查询
StpUtil.getTokenValue(); //获取当前会话的 token 值
StpUtil.getTokenName(); //获取当前`StpLogic`的 token 名称
StpUtil.getLoginIdByToken(String tokenValue); //获取指定token对应的账号id,如果未登录,则返回 null
StpUtil.getTokenTimeout(); //获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
StpUtil.getTokenInfo(); //获取当前会话的 token 信息参数
4、权限认证
所谓权限认证,核心逻辑就是判断一个账号是否拥有指定权限:
有,就让你通过。
没有?那么禁止访问!
4.1、获取当前账号权限码集合 (第四步)
新建一个类,实现 StpInterface接口
/**
*自定义权限加载接口实现类
*/
@Component //保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {
/**
*返回一个账号所拥有的权限码集合
*loginId:账号id,即你在调用 StpUtil.login(id) 时写入的标识值。
*loginType:账号体系标识,此处可以暂时忽略,在 [多账户认证]章节下会对这个概念做详细的解释
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限
List<String> list = new ArrayList<String>();
list.add("101");
list.add("user.add");
list.add("user.update");
list.add("user.get");
// list.add("user.delete");
list.add("art.*");
return list;
}
/**
*返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色
List<String> list = new ArrayList<String>();
list.add("admin");
list.add("super-admin");
return list;
}
}
4.2、权限校验
然后就可以用以下 api 来鉴权了
StpUtil.getPermissionList(); //获取:当前账号所拥有的权限集合
StpUtil.hasPermission("user.add"); //判断:当前账号是否含有指定权限,返回 true 或 false
StpUtil.checkPermission("user.add");//校验:当前账号是否含有指定权限,如果验证未通过,则抛出异常:NotPermissionException
StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get"); //校验:当前账号是否含有指定权限[指定多个,必须全部验证通过]
StpUtil.checkPermissionOr("user.add", "user.delete", "user.get"); //校验:当前账号是否含有指定权限[指定多个,只要其一验证通过即可]
4.3、角色校验
在 Sa-Token 中,角色和权限可以分开独立验证
StpUtil.getRoleList(); //获取:当前账号所拥有的角色集合
StpUtil.hasRole("super-admin"); //判断:当前账号是否拥有指定角色, 返回 true 或 false
StpUtil.checkRole("super-admin"); //校验:当前账号是否含有指定角色标识,如果验证未通过,则抛出异常: NotRoleException
StpUtil.checkRoleAnd("super-admin", "shop-admin"); //校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
StpUtil.checkRoleOr("super-admin", "shop-admin"); //校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
4.4、拦截全局异常 (第五步)
鉴权失败,抛出异常,然后呢?要把异常显示给用户看吗?当然不可以!
你可以创建一个全局异常拦截器,统一返回给前端的格式,参考:
@RestControllerAdvice
public class GlobalExceptionHandler {
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
}
效果:
---------->
4.5、权限通配符
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
5、强制注销&踢人下线
5.1、强制注销
StpUtil.logout(10001); // 强制指定账号注销下线
StpUtil.logout(10001, "PC"); // 强制指定账号指定端注销下线
StpUtil.logoutByTokenValue("token"); // 强制指定 Token 注销下线
5.2、踢人下线
所谓踢人下线,核心操作就是找到指定 loginId 对应的 Token,并设置其失效
StpUtil.kickout(10001); // 将指定账号踢下线
StpUtil.kickout(10001, "PC"); // 将指定账号指定端踢下线
StpUtil.kickoutByTokenValue("token"); // 将指定 Token 踢下线
强制注销和踢人下线的区别
- 强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效
- 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线
6、注解鉴权
尽管使用代码鉴权非常方便,但是我仍希望把鉴权逻辑和业务逻辑分离开来,我可以使用注解鉴权吗?当然可以!
- @SaCheckLogin: 登录校验 —— 只有登录之后才能进入该方法。
- @SaCheckRole("admin"): 角色校验 —— 必须具有指定角色标识才能进入该方法。
- @SaCheckPermission("user:add"): 权限校验 —— 必须具有指定权限才能进入该方法。
- @SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法。
- @SaCheckHttpBasic: HttpBasic校验 —— 只有通过 HttpBasic 认证后才能进入该方法。
- @SaCheckHttpDigest: HttpDigest校验 —— 只有通过 HttpDigest 认证后才能进入该方法。
- @SaIgnore:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。
- @SaCheckDisable("comment"):账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。
6.1、注册拦截器
Sa-Token 使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态
因此,为了使用注解鉴权,你必须手动将 Sa-Token 的全局拦截器注册到你项目中
注意:使用拦截器模式,只能在Controller层进行注解鉴权
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor())
.addPathPatterns("/**");
}
}
如果不生效,可改为extends WebMvcConfigurationSupport
6.2、使用注解鉴权
// 登录校验:只有登录之后才能进入该方法
@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 "查询用户信息";
}
6.3、设定校验模式
@SaCheckRole与@SaCheckPermission注解可设置校验模式,例如:
// 注解式鉴权:只要具有其中一个权限即可通过校验
@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR)
mode有两种取值:
- SaMode.AND,标注一组权限,会话必须全部具有才可通过校验。
- SaMode.OR,标注一组权限,会话只要具有其一即可通过校验。
6.4、角色权限双重 “or校验”
假设有以下业务场景:一个接口在具有权限 user.add 或角色 admin 时可以调通。怎么写?
// 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验
@SaCheckPermission(value = "user.add", orRole = "admin")
orRole 字段代表权限校验未通过时的次要选择,两者只要其一校验成功即可进入请求方法,其有三种写法:
- 写法一:orRole = "admin",代表需要拥有角色 admin 。
- 写法二:orRole = {"admin", "manager", "staff"},代表具有三个角色其一即可。
- 写法三:orRole = {"admin, manager, staff"},代表必须同时具有三个角色。
6.5、忽略认证
使用 @SaIgnore 可表示一个接口忽略认证
@SaCheckLogin
@RestController
public class TestController {
// 此接口加上了 @SaIgnore 可以游客访问
@SaIgnore
@RequestMapping("getList")
public SaResult getList() {
// ...
return SaResult.ok();
}
}
如上代码表示:TestController 中的所有方法都需要登录后才可以访问,但是 getList 接口可以匿名游客访问。
- @SaIgnore 修饰方法时代表这个方法可以被游客访问,修饰类时代表这个类中的所有接口都可以游客访问。
- @SaIgnore 具有最高优先级,当 @SaIgnore 和其它鉴权注解一起出现时,其它鉴权注解都将被忽略。
- @SaIgnore 同样可以忽略掉 Sa-Token 拦截器中的路由鉴权,在下面的 [路由拦截鉴权] 章节中我们会讲到。
6.6、批量注解鉴权
使用 @SaCheckOr 表示批量注解鉴权:
// 在 `@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();
}
7、路由拦截鉴权
需求场景:项目中所有接口均需要登录认证,只有 “登录接口” 本身对外开放
7.1、注册 Sa-Token 路由拦截器
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**")
.excludePathPatterns("/user/doLogin");
}
}
以上代码,我们注册了一个基于 StpUtil.checkLogin() 的登录校验拦截器,并且排除了/user/doLogin接口用来开放登录(除了/user/doLogin以外的所有接口都需要登录才能访问)
7.2、校验函数详解(第六步)
自定义认证规则: new SaInterceptor(handle -> StpUtil.checkLogin()) 是最简单的写法,代表只进行登录校验功能
我们可以往构造函数塞一个完整的 lambda 表达式,来定义详细的校验规则,例如:
@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("/**"); //将自定义拦截器应用于所有路径
}
}
7.3、匹配特征详解
除了上述示例的 path 路由匹配,还可以根据很多其它特征进行匹配,以下是所有可匹配的特征:
// 基础写法样例:匹配一个path,执行一个校验函数
SaRouter.match("/user/**").check(r -> StpUtil.checkLogin());
// 根据 path 路由匹配 ——— 支持写多个path,支持写 restful 风格路由
// 功能说明: 使用 /user , /goods 或者 /art/get 开头的任意路由都将进入 check 方法
SaRouter.match("/user/**", "/goods/**", "/art/get/{id}").check( /* 要执行的校验函数 */ );
// 根据 path 路由排除匹配
// 功能说明: 使用 .html , .css 或者 .js 结尾的任意路由都将跳过, 不会进入 check 方法
SaRouter.match("/**").notMatch("*.html", "*.css", "*.js").check( /* 要执行的校验函数 */ );
// 根据请求类型匹配
SaRouter.match(SaHttpMethod.GET).check( /* 要执行的校验函数 */ );
// 根据一个 boolean 条件进行匹配
SaRouter.match( StpUtil.isLogin() ).check( /* 要执行的校验函数 */ );
// 根据一个返回 boolean 结果的lambda表达式匹配
SaRouter.match( r -> StpUtil.isLogin() ).check( /* 要执行的校验函数 */ );
// 多个条件一起使用
// 功能说明: 必须是 Get 请求 并且 请求路径以 `/user/` 开头
SaRouter.match(SaHttpMethod.GET).match("/user/**").check( /* 要执行的校验函数 */ );
// 可以无限连缀下去
// 功能说明: 同时满足 Get 方式请求, 且路由以 /admin 开头, 路由中间带有 /send/ 字符串, 路由结尾不能是 .js 和 .css
SaRouter
.match(SaHttpMethod.GET)
.match("/admin/**")
.match("/**/send/**")
.notMatch("/**/*.js")
.notMatch("/**/*.css")
// ....
.check( /* 只有上述所有条件都匹配成功,才会执行最后的check校验函数 */ );
7.4、提前退出匹配链
使用 SaRouter.stop() 可以提前退出匹配链,例:
registry.addInterceptor(new SaInterceptor(handler -> {
SaRouter.match("/**").check(r -> System.out.println("进入1"));
SaRouter.match("/**").check(r -> System.out.println("进入2")).stop();
SaRouter.match("/**").check(r -> System.out.println("进入3"));
SaRouter.match("/**").check(r -> System.out.println("进入4"));
SaRouter.match("/**").check(r -> System.out.println("进入5"));
})).addPathPatterns("/**");
代码运行至第2条匹配链时,会在stop函数处提前退出整个匹配函数,从而忽略掉剩余的所有match匹配
除了stop()函数,SaRouter还提供了 back() 函数,用于:停止匹配,结束执行,直接向前端返回结果
// 执行back函数后将停止匹配,也不会进入Controller,而是直接将 back参数 作为返回值输出到前端
SaRouter.match("/user/back").back("要返回到前端的内容");
stop() 与 back() 函数的区别:
- SaRouter.stop() 会停止匹配,进入Controller
- SaRouter.back() 会停止匹配,直接返回结果到前端
7.5、使用free打开一个独立的作用域
free() 的作用是:
打开一个独立的作用域,使内部的 stop() 不再一次性跳出整个 Auth 函数,而是仅仅跳出当前 free 作用域
// 进入 free 独立作用域
SaRouter.match("/**").free(r -> {
SaRouter.match("/a/**").check(/* --- */);
SaRouter.match("/b/**").check(/* --- */).stop();
SaRouter.match("/c/**").check(/* --- */);
});
// 执行 stop() 函数跳出 free 后继续执行下面的 match 匹配
SaRouter.match("/**").check(/* --- */);
7.6、使用注解忽略掉路由拦截校验
我们可以使用 @SaIgnore 注解,忽略掉路由拦截认证
1)先配置好了拦截规则:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handler -> {
// 根据路由划分模块,不同模块不同鉴权
SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
// ...
})).addPathPatterns("/**");
}
2)然后在 Controller 里又添加了忽略校验的注解
@SaIgnore
@RequestMapping("/user/getList")
public SaResult getList() {
System.out.println("------------ 访问进来方法");
return SaResult.ok();
}
请求将会跳过拦截器的校验,直接进入 Controller 的方法中
7.7、关闭注解校验
SaInterceptor 只要注册到项目中,默认就会打开注解校验,如果要关闭此能力,需要:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(
new SaInterceptor(handle -> {
SaRouter.match("/**").check(r -> StpUtil.checkLogin());
}).isAnnotation(false) // 指定关闭掉注解鉴权能力,这样框架就只会做路由拦截校验了
).addPathPatterns("/**");
}
8、Session会话
8.1、Session是什么?
Session 是会话中专业的数据缓存组件,通过 Session 我们可以很方便的缓存一些高频读写数据,提高程序性能,例如:
// 在登录时缓存 user 对象
StpUtil.getSession().set("user", user);
// 然后我们就可以在任意处使用这个 user 对象
SysUser user = (SysUser) StpUtil.getSession().get("user");
在 Sa-Token 中,Session 分为三种,分别是:
- Account-Session: 指的是框架为每个 账号id 分配的 Session
- Token-Session: 指的是框架为每个 token 分配的 Session
- Custom-Session: 指的是以一个 特定的值 作为SessionId,来分配的 Session
8.2、Account-Session
// 获取当前账号 id 的 Account-Session (必须是登录后才能调用)
StpUtil.getSession();
// 获取当前账号 id 的 Account-Session, 并决定在 Session 尚未创建时,是否新建并返回
StpUtil.getSession(true);
// 获取账号 id 为 10001 的 Account-Session
StpUtil.getSessionByLoginId(10001);
// 获取账号 id 为 10001 的 Account-Session, 并决定在 Session 尚未创建时,是否新建并返回
StpUtil.getSessionByLoginId(10001, true);
// 获取 SessionId 为 xxxx-xxxx 的 Account-Session, 在 Session 尚未创建时, 返回 null
StpUtil.getSessionBySessionId("xxxx-xxxx");
8.3、Token-Session
// 获取当前 Token 的 Token-Session 对象
StpUtil.getTokenSession();
// 获取指定 Token 的 Token-Session 对象
StpUtil.getTokenSessionByToken(token);
8.4、Custom-Session
自定义 Session 指的是以一个特定的值作为 SessionId 来分配的Session
例如:以商品 id 作为 key 为每个商品分配一个Session,以便于缓存和商品相关的数据,其相关API如下:
// 查询指定key的Session是否存在
SaSessionCustomUtil.isExists("goods-10001");
// 获取指定key的Session,如果没有,则新建并返回
SaSessionCustomUtil.getSessionById("goods-10001");
// 获取指定key的Session,如果没有,第二个参数决定是否新建并返回
SaSessionCustomUtil.getSessionById("goods-10001", false);
// 删除指定key的Session
SaSessionCustomUtil.deleteSessionById("goods-10001");
8.5、在 Session 上存取值
以上三种 Session 均为框架设计概念上的区分,实际上在获取它们时,返回的都是 SaSession 对象,你可以使用以下 API 在 SaSession 对象上存取值:
// 写值
session.set("name", "zhang");
// 写值 (只有在此key原本无值的时候才会写入)
session.setDefaultValue("name", "zhang");
// 取值
session.get("name");
// 取值 (指定默认值)
session.get("name", "<defaultValue>");
// 取值 (若无值则执行参数方法, 之后将结果保存到此键名下,并返回此结果 若有值则直接返回, 无需执行参数方法)
session.get("name", () -> {
return ...;
});
// ---------- 数据类型转换: ----------
session.getInt("age"); // 取值 (转int类型)
session.getLong("age"); // 取值 (转long类型)
session.getString("name"); // 取值 (转String类型)
session.getDouble("result"); // 取值 (转double类型)
session.getFloat("result"); // 取值 (转float类型)
session.getModel("key", Student.class); // 取值 (指定转换类型)
session.getModel("key", Student.class, <defaultValue>); // 取值 (指定转换类型, 并指定值为Null时返回的默认值)
// 是否含有某个key (返回 true 或 false)
session.has("key");
// 删值
session.delete('name');
// 清空所有值
session.clear();
// 获取此 Session 的所有key (返回Set<String>)
session.keys();
其他操作
// 返回此 Session 的id
session.getId();
// 返回此 Session 的创建时间 (时间戳)
session.getCreateTime();
// 返回此 Session 会话上的底层数据对象(如果更新map里的值,请调用session.update()方法避免产生脏数据)
session.getDataMap();
// 将这个 Session 从持久库更新一下
session.update();
// 注销此 Session 会话 (从持久库删除此Session)
session.logout();
8.6、避免与 HttpSession 混淆使用
SaSession 与 HttpSession 没有任何关系,在HttpSession上写入的值,在SaSession中无法取出
HttpSession并未被框架接管,在使用Sa-Token时,请在任何情况下均使用SaSession,不要使用HttpSession
8.7、未登录场景下获取 Token-Session
默认场景下,只有登录后才能通过 StpUtil.getTokenSession() 获取 Token-Session
如果想要在未登录场景下获取 Token-Session ,有两种方法:
- 方法一:将全局配置项 tokenSessionCheckLogin 改为 false
- 方法二:使用匿名 Token-Session
// 获取当前 Token 的匿名 Token-Session (可在未登录情况下使用的 Token-Session)
StpUtil.getAnonTokenSession();
注意:如果前端没有提交 Token ,或者提交的 Token 是一个无效 Token 的话,框架将不会根据此 Token 创建 Token-Session 对象, 而是随机一个新的 Token 值来创建 Token-Session 对象,此 Token 值可以通过StpUtil.getTokenValue() 获取到。
异常处理
用来获取场景值
package com.questionbrushingplatform.common.exception;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class handlerNotLoginException {
// 全局异常拦截(拦截项目中的NotLoginException异常)
@ExceptionHandler(NotLoginException.class)
@ResponseBody
public SaResult handlerNotLoginException(NotLoginException nle) throws Exception {
// 打印堆栈,以供调试
nle.printStackTrace();
// 判断场景值,自定义异常信息
String message = "";
if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
message = "未能读取到有效 token";
}
else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
message = "token 无效";
}
else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
message = "token 已过期";
}
else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
message = "token 已被顶下线";
}
else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
message = "token 已被踢下线";
}
else if(nle.getType().equals(NotLoginException.TOKEN_FREEZE)) {
message = "token 已被冻结";
}
else if(nle.getType().equals(NotLoginException.NO_PREFIX)) {
message = "未按照指定前缀提交 token";
}
else {
message = "当前会话未登录";
}
// 返回给前端
return SaResult.error(message);
}
}