Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。
Sa-Token 旨在以简单、优雅的方式完成系统的权限认证部分,以登录认证为例,你只需要:
// 会话登录,参数填登录人的账号id StpUtil.login(10001);
无需实现任何接口,无需创建任何配置文件,只需要这一句静态代码的调用,便可以完成会话登录认证。
如果一个接口需要登录后才能访问,我们只需调用以下代码:
// 校验当前客户端是否已经登录,如果未登录则抛出 `NotLoginException` 异常StpUtil.checkLogin();
在 Sa-Token 中,大多数功能都可以一行代码解决:
踢人下线:
// 将账号id为 10077 的会话踢下线 StpUtil.kickout(10077);
权限认证:
// 注解鉴权:只有具备 `user:add` 权限的会话才可以进入方法@SaCheckPermission("user:add") public String insert(SysUser user) { // ... return "用户增加";}
路由拦截鉴权:
// 根据路由划分模块,不同模块不同鉴权 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")); SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); // 更多模块... })).addPathPatterns("/**");
当你受够 Shiro、SpringSecurity 等框架的三拜九叩之后,你就会明白,相对于这些传统老牌框架,Sa-Token 的 API 设计是多么的简单、优雅!
Sa-Token 目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc --><dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.34.0</version></dependency>
注:如果你使用的 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。
你可以零配置启动项目 ,但同时你也可以在 application.yml 中增加如下配置,定制性使用框架:
server: # 端口 port: 8081############## Sa-Token 配置 (文档: https://sa-token.cc) ##############sa-token: # token名称 (同时也是cookie名称) token-name: satoken # token有效期,单位s 默认30天, -1代表永不过期 timeout: 2592000 # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 activity-timeout: -1 # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) is-share: true # token风格 token-style: uuid # 是否输出操作日志 is-log: false
@RestController @RequestMapping("/user/") public class UserController { // 测试登录,浏览器访问:http://localhost:8081/user/doLogin?username=zhang&password=123456 @RequestMapping("doLogin") public String doLogin(String username, String password) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("iron".equals(username) && "123456".equals(password)) { StpUtil.login(10001); return "登录成功"; } return "登录失败"; } // 查询登录状态,浏览器访问:http://localhost:8081/user/isLogin @RequestMapping("isLogin") public String isLogin() { return "当前会话是否登录:" + StpUtil.isLogin(); } }
对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验:
那么,判断会话是否登录的依据是什么?我们先来简单分析一下登录访问流程:
所谓登录认证,指的就是服务器校验账号密码,为用户颁发 Token 会话凭证的过程,这个 Token 也是我们后续判断会话是否登录的关键所在。
根据以上思路,我们需要一个会话登录的函数:
// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等StpUtil.login(Object id);
只此一句代码,便可以使会话登录成功,实际上,Sa-Token 在背后做了大量的工作,包括但不限于:
你暂时不需要完整的了解整个登录过程,你只需要记住关键一点:Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端。
所以一般情况下,我们的登录接口代码,会大致类似如下:
// 会话登录接口 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 第一步:比对前端提交的账号名称、密码 if("iron".equals(name) && "123456".equals(pwd)) { // 第二步:根据账号id,进行登录 StpUtil.login(10001); return SaResult.ok("登录成功"); } return SaResult.error("登录失败");}
如果你对以上代码阅读没有压力,你可能会注意到略显奇怪的一点:此处仅仅做了会话登录,但并没有主动向前端返回 Token 信息。是因为不需要吗?严格来讲是需要的,只不过 StpUtil.login(id) 方法利用了 Cookie 自动注入的特性,省略了你手写返回 Token 的代码。
如果你对 Cookie 功能还不太了解,也不用担心,我们会在之后的 [ 前后端分离 ] 章节中详细的阐述 Cookie 功能,现在你只需要了解最基本的两点:
因此,在 Cookie 功能的加持下,我们可以仅靠 StpUtil.login(id) 一句代码就完成登录认证。
除了登录方法,我们还需要:
// 当前会话注销登录StpUtil.logout();// 获取当前会话是否已经登录,返回true=已登录,false=未登录StpUtil.isLogin();// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`StpUtil.checkLogin();
异常 NotLoginException 代表当前会话暂未登录,可能的原因有很多:前端没有提交 Token、前端提交的 Token 是无效的、前端提交的 Token 已经过期 …… 等等,可参照此篇:未登录场景值,了解如何获取未登录的场景值。
// 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`StpUtil.getLoginId();// 类似查询API还有:StpUtil.getLoginIdAsString(); // 获取当前会话账号id, 并转化为`String`类型StpUtil.getLoginIdAsInt(); // 获取当前会话账号id, 并转化为`int`类型StpUtil.getLoginIdAsLong(); // 获取当前会话账号id, 并转化为`long`类型// ---------- 指定未登录情形下返回的默认值 ----------// 获取当前会话账号id, 如果未登录,则返回null StpUtil.getLoginIdDefaultNull();// 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)StpUtil.getLoginId(T defaultValue);
// 获取当前会话的token值StpUtil.getTokenValue();// 获取当前`StpLogic`的token名称StpUtil.getTokenName();// 获取指定token对应的账号id,如果未登录,则返回 nullStpUtil.getLoginIdByToken(String tokenValue);// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)StpUtil.getTokenTimeout();// 获取当前会话的token信息参数StpUtil.getTokenInfo();
有关TokenInfo参数详解,请参考:TokenInfo参数详解
新建 LoginController,复制以下代码
/** * 登录测试 */@RestController@RequestMapping("/acc/")public class LoginController { // 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if("iron".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功"); } return SaResult.error("登录失败"); } // 查询登录状态 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登录:" + StpUtil.isLogin()); } // 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 测试注销 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); }}
所谓权限认证,核心逻辑就是判断一个账号是否拥有指定权限:
深入到底层数据中,就是每个账号都会拥有一个权限码集合,框架来校验这个集合中是否包含指定的权限码。
例如:当前账号拥有权限码集合 ["user-add", "user-delete", "user-get"],这时候我来校验权限 "user-update",则其结果就是:验证失败,禁止访问。
所以现在问题的核心就是:
因为每个项目的需求不同,其权限设计也千变万化,因此 [ 获取当前账号权限码集合 ] 这一操作不可能内置到框架中, 所以 Sa-Token 将此操作以接口的方式暴露给你,以方便你根据自己的业务逻辑进行重写。
你需要做的就是新建一个类,实现 StpInterface接口,例如以下代码:
/** * 自定义权限验证接口扩展 */@Component // 保证此类被SpringBoot扫描,完成Sa-Token的自定义权限验证扩展 public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @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; }}
参数解释:
可参考代码:码云:StpInterfaceImpl.java
注意: StpInterface 接口在需要鉴权时由框架自动调用,开发者只需要配置好就可以使用下面的鉴权方法或后面的注解鉴权
然后就可以用以下api来鉴权了
// 获取:当前账号所拥有的权限集合StpUtil.getPermissionList();// 判断:当前账号是否含有指定权限, 返回 true 或 falseStpUtil.hasPermission("user.add"); // 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException StpUtil.checkPermission("user.add"); // 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get"); // 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");
扩展:NotPermissionException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常
在Sa-Token中,角色和权限可以独立验证
// 获取:当前账号所拥有的角色集合StpUtil.getRoleList();// 判断:当前账号是否拥有指定角色, 返回 true 或 falseStpUtil.hasRole("super-admin"); // 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleExceptionStpUtil.checkRole("super-admin"); // 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]StpUtil.checkRoleAnd("super-admin", "shop-admin"); // 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] StpUtil.checkRoleOr("super-admin", "shop-admin");
扩展:NotRoleException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常
Sa-Token允许你根据通配符指定泛权限,例如当一个账号拥有art.*的权限时,art.add、art.delete、art.update都将匹配通过
// 当拥有 art.* 权限时StpUtil.hasPermission("art.add"); // trueStpUtil.hasPermission("art.update"); // trueStpUtil.hasPermission("goods.add"); // false// 当拥有 *.delete 权限时StpUtil.hasPermission("art.delete"); // trueStpUtil.hasPermission("user.delete"); // trueStpUtil.hasPermission("user.update"); // false// 当拥有 *.js 权限时StpUtil.hasPermission("index.js"); // trueStpUtil.hasPermission("index.css"); // falseStpUtil.hasPermission("index.html"); // false
上帝权限:当一个账号拥有 "*" 权限时,他可以验证通过任何权限码 (角色认证同理)
权限精确到按钮级的意思就是指:权限范围可以控制到页面上的每一个按钮是否显示。
思路:如此精确的范围控制只依赖后端已经难以完成,此时需要前端进行一定的逻辑判断。
如果是前后端一体项目,可以参考:Thymeleaf 标签方言,如果是前后端分离项目,则:
<button v-if="arr.indexOf('user.delete') > -1">删除按钮</button>
其中:arr是当前用户拥有的权限码数组,user.delete是显示按钮需要拥有的权限码,删除按钮是用户拥有权限码才可以看到的内容。
注意:以上写法只为提供一个参考示例,不同框架有不同写法,大家可根据项目技术栈灵活封装进行调用。
所谓踢人下线,核心操作就是找到指定 loginId 对应的 Token,并设置其失效。
StpUtil.logout(10001); // 强制指定账号注销下线 StpUtil.logout(10001, "PC"); // 强制指定账号指定端注销下线 StpUtil.logoutByTokenValue("token"); // 强制指定 Token 注销下线
StpUtil.kickout(10001); // 将指定账号踢下线 StpUtil.kickout(10001, "PC"); // 将指定账号指定端踢下线StpUtil.kickoutByTokenValue("token"); // 将指定 Token 踢下线
强制注销 和 踢人下线 的区别在于:
有同学表示:尽管使用代码鉴权非常方便,但是我仍希望把鉴权逻辑和业务逻辑分离开来,我可以使用注解鉴权吗?当然可以!
注解鉴权 —— 优雅的将鉴权与业务代码分离!
Sa-Token 使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态因此,为了使用注解鉴权,你必须手动将 Sa-Token 的全局拦截器注册到你项目中
以SpringBoot2.0为例,新建配置类SaTokenConfigure.java
@Configurationpublic class SaTokenConfigure implements WebMvcConfigurer { // 注册 Sa-Token 拦截器,打开注解式鉴权功能 @Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器,打开注解式鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); }}
保证此类被springboot启动类扫描到即可
然后我们就可以愉快的使用注解鉴权了:
// 登录校验:只有登录之后才能进入该方法 @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 校验:只有通过 Basic 认证后才能进入该方法 @SaCheckBasic(account = "sa:123456") @RequestMapping("add") public String add() { return "用户增加";}// 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法 @SaCheckDisable("comment") @RequestMapping("send") public String send() { return "查询用户信息";}
注:以上注解都可以加在类上,代表为这个类所有方法进行鉴权
@SaCheckRole与@SaCheckPermission注解可设置校验模式,例如:
// 注解式鉴权:只要具有其中一个权限即可通过校验 @RequestMapping("atJurOr")@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR) public SaResult atJurOr() { return SaResult.data("用户信息");}
mode有两种取值:
假设有以下业务场景:一个接口在具有权限 user.add 或角色 admin 时可以调通。怎么写?
// 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验@RequestMapping("userAdd")@SaCheckPermission(value = "user.add", orRole = "admin") public SaResult userAdd() { return SaResult.data("用户信息");}
orRole 字段代表权限认证未通过时的次要选择,两者只要其一认证成功即可通过校验,其有三种写法:
使用 @SaIgnore 可表示一个接口忽略认证:
@SaCheckLogin@RestControllerpublic class TestController { // ... 其它方法 // 此接口加上了 @SaIgnore 可以游客访问 @SaIgnore @RequestMapping("getList") public SaResult getList() { // ... return SaResult.ok(); }}
如上代码表示:TestController 中的所有方法都需要登录后才可以访问,但是 getList 接口可以匿名游客访问。
疑问:我能否将注解写在其它架构层呢,比如业务逻辑层?
使用拦截器模式,只能在Controller层进行注解鉴权,如需在任意层级使用注解鉴权,请参考:AOP注解鉴权
sa-token: # token前缀 token-prefix: Bearer
Sa-Token默认的token生成策略是uuid风格,其模样类似于:623368f0-ae5e-4475-a53f-93e4225f16ae。如果你对这种风格不太感冒,还可以将token生成设置为其他风格。
怎么设置呢?只需要在yml配置文件里设置 sa-token.token-style=风格类型 即可,其有多种取值:
// 1. token-style=uuid —— uuid风格 (默认风格)"623368f0-ae5e-4475-a53f-93e4225f16ae"// 2. token-style=simple-uuid —— 同上,uuid风格, 只不过去掉了中划线"6fd4221395024b5f87edd34bc3258ee8"// 3. token-style=random-32 —— 随机32位字符串"qEjyPsEA1Bkc9dr8YP6okFr5umCZNR6W"// 4. token-style=random-64 —— 随机64位字符串"v4ueNLEpPwMtmOPMBtOOeIQsvP8z9gkMgIVibTUVjkrNrlfra5CGwQkViDjO8jcc"// 5. token-style=random-128 —— 随机128位字符串"nojYPmcEtrFEaN0Otpssa8I8jpk8FO53UcMZkCP9qyoHaDbKS6dxoRPky9c6QlftQ0pdzxRGXsKZmUSrPeZBOD6kJFfmfgiRyUmYWcj4WU4SSP2ilakWN1HYnIuX0Olj"// 6. token-style=tik —— tik风格"gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__"
如果你觉着以上风格都不是你喜欢的类型,那么你还可以自定义token生成策略,来定制化token生成风格。
怎么做呢?只需要重写 SaStrategy 策略类的 createToken 算法即可:
1、在SaTokenConfigure配置类中添加代码:
@Configurationpublic class SaTokenConfigure { /** * 重写 Sa-Token 框架内部算法策略 */ @Autowired public void rewriteSaStrategy() { // 重写 Token 生成策略 SaStrategy.me.createToken = (loginId, loginType) -> { return SaFoxUtil.getRandomString(60); // 随机60位长度字符串 }; }}
2、再次调用 StpUtil.login(10001)方法进行登录,观察其生成的token样式:
fuPSwZsnUhwgz08GTCH4wOgasWtc3odP4HLwXJ7NDGOximTvT4OlW19zeLH
首先在项目已经引入 Sa-Token 的基础上,继续添加:
<!-- Sa-Token 整合 jwt --><dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-jwt</artifactId> <version>1.34.0</version></dependency>
注意: sa-token-jwt 显式依赖 hutool-jwt 5.7.14 版本,意味着:你的项目中要么不引入 Hutool,要么引入版本 >= 5.7.14 的 Hutool 版本
在 application.yml 配置文件中配置 jwt 生成秘钥:
sa-token: # jwt秘钥 jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk
根据不同的整合规则,插件提供了三种不同的模式,你需要 选择其中一种 注入到你的项目中
@Configurationpublic class SaTokenConfigure { // Sa-Token 整合 jwt (Simple 简单模式) @Bean public StpLogic getStpLogicJwt() { return new StpLogicJwtForSimple(); } // Sa-Token 整合 jwt (Mixin 混入模式) @Bean public StpLogic getStpLogicJwt() { return new StpLogicJwtForMixin(); } // Sa-Token 整合 jwt (Stateless 无状态模式) @Bean public StpLogic getStpLogicJwt() { return new StpLogicJwtForStateless(); }}
注入不同模式会让框架具有不同的行为策略,以下是三种模式的差异点(为方便叙述,以下比较以同时引入 jwt 与 Redis 作为前提):
功能点 | Simple 简单模式 | Mixin 混入模式 | Stateless 无状态模式 |
Token风格 | jwt风格 | jwt风格 | jwt风格 |
登录数据存储 | Redis中 | Token中 | Token中 |
Session存储 | Redis中 | Redis中 | 无Session |
注销下线 | 前后端双清数据 | 前后端双清数据 | 前端清除数据 |
踢人下线API | 支持 | 不支持 | 不支持 |
顶人下线API | 支持 | 不支持 | 不支持 |
登录认证 | 支持 | 支持 | 支持 |
角色认证 | 支持 | 支持 | 支持 |
权限认证 | 支持 | 支持 | 支持 |
timeout 有效期 | 支持 | 支持 | 支持 |
activity-timeout 有效期 | 支持 | 支持 | 不支持 |
id反查Token | 支持 | 支持 | 不支持 |
会话管理 | 支持 | 部分支持 | 不支持 |
注解鉴权 | 支持 | 支持 | 支持 |
路由拦截鉴权 | 支持 | 支持 | 支持 |
账号封禁 | 支持 | 支持 | 不支持 |
身份切换 | 支持 | 支持 | 支持 |
二级认证 | 支持 | 支持 | 支持 |
模式总结 | Token风格替换 | jwt 与 Redis 逻辑混合 | 完全舍弃Redis,只用jwt |
你可以通过以下方式在登录时注入扩展参数:
// 登录10001账号,并为生成的 Token 追加扩展参数nameStpUtil.login(10001, SaLoginConfig.setExtra("name", "zhangsan"));// 连缀写法追加多个StpUtil.login(10001, SaLoginConfig .setExtra("name", "zhangsan") .setExtra("age", 18) .setExtra("role", "超级管理员"));// 获取扩展参数 String name = StpUtil.getExtra("name");// 获取任意 Token 的扩展参数 String name = StpUtil.getExtra("tokenValue", "name");
sa-token-jwt 插件默认只为 StpUtil 注入 StpLogicJwtFoxXxx 实现,自定义的 StpUserUtil 是不会自动注入的,我们需要帮其手动注入:
/** * 为 StpUserUtil 注入 StpLogicJwt 实现 */@Autowiredpublic void setUserStpLogic() { StpUserUtil.setStpLogic(new StpLogicJwtForSimple(StpUserUtil.TYPE));}
如果需要自定义生成 token 的算法(例如更换sign方式),直接重写 SaJwtTemplate 对象即可:
/** * 自定义 SaJwtUtil 生成 token 的算法 */@Autowiredpublic void setSaJwtTemplate() { SaJwtUtil.setSaJwtTemplate(new SaJwtTemplate() { @Override public String generateToken(JWT jwt, String keyt) { System.out.println("------ 自定义了 token 生成算法"); return super.generateToken(jwt, keyt); } });}
1、使用 jwt-simple 模式后,is-share=false 恒等于 false。
is-share=true 的意思是每次登录都产生一样的 token,这种策略和 [ 为每个 token 单独设定 setExtra 数据 ] 不兼容的, 为保证正确设定 Extra 数据,当使用 jwt-simple 模式后,is-share 配置项 恒等于 false。
2、使用 jwt-mixin 模式后,is-concurrent 必须为 true。
is-concurrent=false 代表每次登录都把旧登录顶下线,但是 jwt-mixin 模式登录的 token 并不会记录在持久库数据中, 技术上来讲无法将其踢下线,所以此时顶人下线和踢人下线等 API 都属于不可用状态,所以此时 is-concurrent 配置项必须配置为 true。
Sa-Token 提供一种侦听器机制,通过注册侦听器,你可以订阅框架的一些关键性事件,例如:用户登录、退出、被踢下线等。
事件触发流程大致如下:
框架默认内置了侦听器 SaTokenListenerForLog 实现:代码参考 ,功能是控制台 log 打印输出,你可以通过配置sa-token.is-log=true开启。
要注册自定义的侦听器也非常简单:
新建MySaTokenListener.java,实现SaTokenListener接口,并添加上注解@Component,保证此类被SpringBoot扫描到:
/** * 自定义侦听器的实现 */@Componentpublic class MySaTokenListener implements SaTokenListener { /** 每次登录时触发 */ @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) { System.out.println("---------- 自定义侦听器实现 doLogin"); } /** 每次注销时触发 */ @Override public void doLogout(String loginType, Object loginId, String tokenValue) { System.out.println("---------- 自定义侦听器实现 doLogout"); } /** 每次被踢下线时触发 */ @Override public void doKickout(String loginType, Object loginId, String tokenValue) { System.out.println("---------- 自定义侦听器实现 doKickout"); } /** 每次被顶下线时触发 */ @Override public void doReplaced(String loginType, Object loginId, String tokenValue) { System.out.println("---------- 自定义侦听器实现 doReplaced"); } /** 每次被封禁时触发 */ @Override public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) { System.out.println("---------- 自定义侦听器实现 doDisable"); } /** 每次被解封时触发 */ @Override public void doUntieDisable(String loginType, Object loginId, String service) { System.out.println("---------- 自定义侦听器实现 doUntieDisable"); } /** 每次二级认证时触发 */ @Override public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) { System.out.println("---------- 自定义侦听器实现 doOpenSafe"); } /** 每次退出二级认证时触发 */ @Override public void doCloseSafe(String loginType, String tokenValue, String service) { System.out.println("---------- 自定义侦听器实现 doCloseSafe"); } /** 每次创建Session时触发 */ @Override public void doCreateSession(String id) { System.out.println("---------- 自定义侦听器实现 doCreateSession"); } /** 每次注销Session时触发 */ @Override public void doLogoutSession(String id) { System.out.println("---------- 自定义侦听器实现 doLogoutSession"); } /** 每次Token续期时触发 */ @Override public void doRenewTimeout(String tokenValue, Object loginId, long timeout) { System.out.println("---------- 自定义侦听器实现 doRenewTimeout"); }}
以上代码由于添加了 @Component 注解,会被 SpringBoot 扫描并自动注册到事件中心,此时我们无需手动注册。
如果我们没有添加 @Component 注解或者项目属于非 IOC 自动注入环境,则需要我们手动将这个侦听器注册到事件中心:
// 将侦听器注册到事件发布中心SaTokenEventCenter.registerListener(new MySaTokenListener());
事件中心的其它一些常用方法:
// 获取已注册的所有侦听器 SaTokenEventCenter.getListenerList(); // 重置侦听器集合 SaTokenEventCenter.setListenerList(listenerList); // 注册一个侦听器 SaTokenEventCenter.registerListener(listener); // 注册一组侦听器 SaTokenEventCenter.registerListenerList(listenerList); // 移除一个侦听器 SaTokenEventCenter.removeListener(listener); // 移除指定类型的所有侦听器 SaTokenEventCenter.removeListener(cls); // 清空所有已注册的侦听器 SaTokenEventCenter.clearListener(); // 判断是否已经注册了指定侦听器 SaTokenEventCenter.hasListener(listener); // 判断是否已经注册了指定类型的侦听器 SaTokenEventCenter.hasListener(cls);
在 TestController 中添加登录测试代码:
// 测试登录接口 @RequestMapping("login")public SaResult login() { System.out.println("登录前"); StpUtil.login(10001); System.out.println("登录后"); return SaResult.ok();}
@Componentpublic class MySaTokenListener extends SaTokenListenerForSimple { /* * SaTokenListenerForSimple 对所有事件提供了空实现,通过继承此类,你只需重写一部分方法即可实现一个可用的侦听器。 */ /** 每次登录时触发 */ @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) { System.out.println("---------- 自定义侦听器实现 doLogin"); }}3.2、使用匿名内部类的方式注册:// 登录时触发 SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() { @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) { System.out.println("---------------- doLogin"); }});
如果你认为你的事件处理代码是不安全的(代码可能在运行时抛出异常),则需要使用 try-catch 包裹代码,以防因为抛出异常导致 Sa-Token 的整个登录流程被强制中断。
// 登录时触发 SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() { @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) { try { // 不安全代码需要写在 try-catch 里 // ...... } catch (Exception e) { e.printStackTrace(); } }});
可以,多个侦听器间彼此独立,互不影响,按照注册顺序依次接受到事件通知。
功能点 | SSO单点登录 | OAuth2.0 |
统一认证 | 支持度高 | 支持度高 |
统一注销 | 支持度高 | 支持度低 |
多个系统会话一致性 | 强一致 | 弱一致 |
第三方应用授权管理 | 不支持 | 支持度高 |
自有系统授权管理 | 支持度高 | 支持度低 |
Client级的权限校验 | 不支持 | 支持度高 |
集成简易度 | 比较简单 | 难度中等 |
注:以上仅为在 Sa-Token 中两种技术的差异度比较,不同框架的实现可能略有差异,但整体思想是一致的。
举个场景,假设我们的系统被切割为N个部分:商城、论坛、直播、社交…… 如果用户每访问一个模块都要登录一次,那么用户将会疯掉, 为了优化用户体验,我们急需一套机制将这N个系统的认证授权互通共享,让用户在一个系统登录之后,便可以畅通无阻的访问其它所有系统。
单点登录——就是为了解决这个问题而生!
简而言之,单点登录可以做到:在多个互相信任的系统中,用户只需登录一次,就可以访问所有系统。
Sa-Token-SSO 由简入难划分为三种模式,解决不同架构下的 SSO 接入问题:
系统架构 | 采用模式 | 简介 | 文档链接 |
前端同域 + 后端同 Redis | 模式一 | 共享 Cookie 同步会话 | 文档 、示例 |
前端不同域 + 后端同 Redis | 模式二 | URL重定向传播会话 | 文档 、示例 |
前端不同域 + 后端不同 Redis | 模式三 | Http请求获取会话 | 文档 、示例 |
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc --><dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.34.0</version></dependency><!-- Sa-Token 插件:整合SSO --><dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-sso</artifactId> <version>1.34.0</version></dependency><!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) --><dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-dao-redis-jackson</artifactId> <version>1.34.0</version></dependency><dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId></dependency><!-- 视图引擎(在前后端不分离模式下提供视图支持) --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><!-- Http请求工具(在模式三的单点注销功能下用到,如不需要可以注释掉) --><dependency> <groupId>com.dtflys.forest</groupId> <artifactId>forest-spring-boot-starter</artifactId> <version>1.5.26</version></dependency>
除了 sa-token-spring-boot-starter 和 sa-token-sso 以外,其它包都是可选的:
建议先完整测试三种模式之后再对pom依赖进行酌情删减。
/** * Sa-Token-SSO Server端 Controller */@RestControllerpublic class SsoServerController { /* * SSO-Server端:处理所有SSO相关请求 (下面的章节我们会详细列出开放的接口) */ @RequestMapping("/sso/*") public Object ssoRequest() { return SaSsoProcessor.instance.serverDister(); } /** * 配置SSO相关参数 */ @Autowired private void configSso(SaSsoConfig sso) { // 配置:未登录时返回的View sso.setNotLoginView(() -> { String msg = "当前会话在SSO-Server端尚未登录,请先访问" + "<a href='/sso/doLogin?name=sa&pwd=123456' target='_blank'> doLogin登录 </a>" + "进行登录之后,刷新页面开始授权"; return msg; }); // 配置:登录处理函数 sso.setDoLoginHandle((name, pwd) -> { // 此处仅做模拟登录,真实环境应该查询数据进行登录 if("sa".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue()); } return SaResult.error("登录失败!"); }); // 配置 Http 请求处理器 (在模式三的单点注销功能下用到,如不需要可以注释掉) sso.setSendHttp(url -> { try { // 发起 http 请求 System.out.println("------ 发起请求:" + url); return Forest.get(url).executeAsString(); } catch (Exception e) { e.printStackTrace(); return null; } }); }}
# 端口server: port: 9000# Sa-Token 配置sa-token: # ------- SSO-模式一相关配置 (非模式一不需要配置) # cookie: # 配置 Cookie 作用域 # domain: stp.com # ------- SSO-模式二相关配置 sso: # Ticket有效期 (单位: 秒),默认五分钟 ticket-timeout: 300 # 所有允许的授权回调地址 allow-url: "*" # 是否打开单点注销功能 is-slo: true # ------- SSO-模式三相关配置 (下面的配置在SSO模式三并且 is-slo=true 时打开) # 是否打开模式三 isHttp: true # 接口调用秘钥(用于SSO模式三的单点注销功能) secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # ---- 除了以上配置项,你还需要为 Sa-Token 配置http请求处理器(文档有步骤说明) spring: # Redis配置 (SSO模式一和模式二使用Redis来同步会话) redis: # Redis数据库索引(默认为0) database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: forest: # 关闭 forest 请求日志打印 log-enabled: false
访问统一授权地址:
可以看到这个页面目前非常简陋,这是因为我们以上的代码示例,主要目标是为了带大家从零搭建一个可用的SSO认证服务端,所以就对一些不太必要的步骤做了简化。
本文链接:http://www.28at.com/showinfo-26-35341-0.html再见,Shiro !你好,Sa-Token!
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com