官方文档:https://sa-token.cc/doc.html#/start/example
参照官方文档,做如下实验:
整合SpringBoot3
登录认证:
权限认证:https://sa-token.cc/doc.html#/use/jur-auth
AOP注解权限:https://sa-token.cc/doc.html#/plugin/aop-at
集成Redis:https://sa-token.cc/doc.html#/up/integ-redis
前后分离:https://sa-token.cc/doc.html#/up/not-cookie
自定义Token:https://sa-token.cc/doc.html#/up/token-style
二级认证:https://sa-token.cc/doc.html#/up/safe-auth
快速入门 创建项目 创建SpringBoot项目。导入sa-token依赖
<dependency > <groupId > cn.dev33</groupId > <artifactId > sa-token-spring-boot3-starter</artifactId > <version > 1.44.0</version > </dependency >
配置文件 server: port: 8080 sa-token: token-name: satoken timeout: 2592000 active-timeout: -1 is-concurrent: true is-share: false token-style: uuid is-log: true
启动类 @Slf4j @SpringBootApplication public class SaTokenMainApplication { public static void main (String [] args ) { SpringApplication .run (SaTokenMainApplication .class , args); SaTokenConfig config = SaManager .getConfig (); log.info ("SaTokenMainApplication启动成功,配置:{}" ,config); } }
测试Controller 编写两个模拟方法。暂时不用连数据库,简单测试功能即可
模拟登录 :只要是 admin /123456 就代表登录成功
判断登录状态 :快速判断当前浏览器对应的用户是否是登录状态
package com.lfy.satoken.controller;import cn.dev33.satoken.stp.StpUtil;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/user/") public class UserController { @RequestMapping("doLogin") public String doLogin (String username, String password) { if ("admin" .equals(username) && "123456" .equals(password)) { StpUtil.login(10001 ); return "登录成功" ; } return "登录失败" ; } @RequestMapping("isLogin") public String isLogin () { return "当前会话是否登录:" + StpUtil.isLogin(); } @RequestMapping("logout") public String logout () { StpUtil.logout(); return "用户已退出登录" ; } }
原理
进阶实验:token 引入Redis,统一存储Session数据 Sa-Token 默认将数据保存在内存中 ,此模式读写速度最快 ,且避免了序列化与反序列化 带来的性能消耗,但是此模式也有一些缺点 ,比如:
重启后数据会丢失 。
无法在分布式环境中共享数据 。
引入依赖 <dependency > <groupId > cn.dev33</groupId > <artifactId > sa-token-redis-template</artifactId > <version > 1.44.0</version > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-pool2</artifactId > </dependency >
序列化框架 默认使用 jackson (SpringBoot 底层自带的 json转换框架)
可以导入fastjson2,作为替换;(fastjson第一版漏洞太多了)
<dependency > <groupId > cn.dev33</groupId > <artifactId > sa-token-fastjson2</artifactId > <version > 1.44.0</version > </dependency >
Redis连接配置 修改application.yml
spring: data: redis: host: localhost port: 6379 password: ruoyi123
请求测试
先测试:http://localhost:10008/user/isLogin 显示未登录
再发送:http://localhost:10008/user/doLogin?username=admin&password=123456 显示登录成功
再发送:http://localhost:10008/user/isLogin 显示已登录
查看redis数据;且每个数据有过期时间
自定义token风格 内置风格
Sa-Token 默认的 token 生成策略是 uuid 风格,其模样类似于:623368f0-ae5e-4475-a53f-93e4225f16ae。如果你对这种风格不太感冒,还可以将 token 生成设置为其他风格。
只需要在yml配置文件里设置 sa-token.token-style=风格类型 即可,其有多种取值:
"623368f0-ae5e-4475-a53f-93e4225f16ae" "6fd4221395024b5f87edd34bc3258ee8" "qEjyPsEA1Bkc9dr8YP6okFr5umCZNR6W" "v4ueNLEpPwMtmOPMBtOOeIQsvP8z9gkMgIVibTUVjkrNrlfra5CGwQkViDjO8jcc" "nojYPmcEtrFEaN0Otpssa8I8jpk8FO53UcMZkCP9qyoHaDbKS6dxoRPky9c6QlftQ0pdzxRGXsKZmUSrPeZBOD6kJFfmfgiRyUmYWcj4WU4SSP2ilakWN1HYnIuX0Olj" "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__"
自定义风格 只需要重写 SaStrategy 策略类的 createToken 算法即可;
@Slf4j @Configuration public class SaTokenConfigure { @PostConstruct public void rewriteSaStrategy () { SaStrategy.instance.createToken = (loginId, loginType) -> { log.info("loginId={}, loginType={}" , loginId, loginType); return "aaabbb_" +SaFoxUtil.getRandomString(10 ); }; } }
集成jwt 引入依赖
注意: sa-token-jwt 显式依赖 hutool-jwt 5.7.14 版本,保险起见:你的项目中要么不引入 hutool,要么引入版本 >= 5.7.14 的 hutool 版本。
hutool 5.8.13 和 5.8.14 版本下会出现类型转换问题
<dependency > <groupId > cn.dev33</groupId > <artifactId > sa-token-jwt</artifactId > <version > 1.44.0</version > </dependency >
配置密钥 sa-token: # jwt秘钥 jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk
注入jwt实现(三种模式) Simple简单模式 @Configuration public class SaTokenConfigure { @Bean public StpLogic getStpLogicJwt () { return new StpLogicJWTForSimple (); } }
Mixin 混入模式 @Configuration public class SaTokenConfigure { @Bean public StpLogic getStpLogicJwt ( ) { return new StpLogicJwtForMixin (); } }
Stateless 模式 服务器完全无状态模式。
@Configuration public class SaTokenConfigure { @Bean public StpLogic getStpLogicJwt ( ) { return new StpLogicJwtForStateless (); } }
登录时额外数据 SaLoginParameter 的所有extra数据,会在生成jwt的时候带上
SaLoginParameter loginParameter = new SaLoginParameter ();loginParameter.setExtra("username" ,"admin" ); loginParameter.setExtra("email" ,"admin@qq.coom" ); StpUtil.login(10001 ,loginParameter);
进阶实验:功能 登录认证
StpUtil:常用方法 登录&注销 StpUtil.login(Object id); StpUtil.logout(); StpUtil.isLogin(); StpUtil.checkLogin();
异常 NotLoginException 代表当前会话暂未登录,可能的原因有很多: 前端没有提交 token、前端提交的 token 是无效的、前端提交的 token 已经过期 …… 等等
会话查询 StpUtil.getLoginId(); StpUtil.getLoginIdAsString(); StpUtil.getLoginIdAsInt(); StpUtil.getLoginIdAsLong(); StpUtil.getLoginIdDefaultNull(); StpUtil.getLoginId(T defaultValue);
token查询 StpUtil.getTokenValue(); StpUtil.getTokenName(); StpUtil.getLoginIdByToken(String tokenValue); StpUtil.getTokenTimeout(); StpUtil.getTokenInfo();
权限认证 认证流程 所谓权限认证 ,核心逻辑就是判断一个账号是否拥有指定权限 :
深入到底层数据中,就是每个账号 都会拥有一组权限码 集合,框架来校验这个集合中是否包含指定的权限码。
例如:当前账号拥有权限码 集合 ["user-add", "user-delete", "user-get"],这时候我来校验权限 "user-update",则其结果就是:验证失败,禁止访问。
所以现在问题的核心就是两个 :
如何获取一个账号 所拥有的权限码集合 ?
权限码的集合其实在数据库有对应表关系。
用户登录进来要自己去数据库查询
查询出的所有权限,sa-token将会拿到,将来用它们进行匹配
本次操作需要验证的权限码是哪个 ?
每个方法对应哪些权限码,是业务提前定死
每次请求进来,用方法的权限码,和用户的权限码列表进行匹配
匹配上则放行
将来每个方法上都会标注这个方法的权限码。每次请求进行匹配校验。
@CheckPerssion(“user:get:info”)
获取用户权限码集合
用户登录以后,Sa-Token框架要有能力获取到用户所有的权限集合与角色列表。这样以后才能匹配
因为每个项目的需求不同,其权限设计也千变万化,因此 [ 获取当前账号权限码集合 ] 这一操作不可能内置到框架中, 所以 Sa-Token 将此操作以接口的方式暴露给你,以方便你根据自己的业务逻辑进行重写。
你需要做的就是新建一个类,实现 StpInterface接口,例如以下代码:
@Component public class StpInterfaceImpl implements StpInterface { @Override public List <String > getPermissionList (Object loginId, String loginType ) { List <String > list = new ArrayList <String >(); list.add ("101" ); list.add ("user.add" ); list.add ("user.update" ); list.add ("user.get" ); list.add ("art.*" ); return list; } @Override public List <String > getRoleList (Object loginId, String loginType ) { List <String > list = new ArrayList <String >(); list.add ("admin" ); list.add ("super-admin" ); return list; } }
权限校验 StpUtil.getPermissionList(); StpUtil.hasPermission("user.add" ); StpUtil.checkPermission("user.add" ); StpUtil.checkPermissionAnd("user.add" , "user.delete" , "user.get" ); StpUtil.checkPermissionOr("user.add" , "user.delete" , "user.get" );
角色校验 在 Sa-Token 中,角色和权限可以分开独立验证
StpUtil.getRoleList(); StpUtil.hasRole("super-admin" ); StpUtil.checkRole("super-admin" ); StpUtil.checkRoleAnd("super-admin" , "shop-admin" ); StpUtil.checkRoleOr("super-admin" , "shop-admin" );
全局异常处理 有同学要问,鉴权失败,抛出异常,然后呢?要把异常显示给用户看吗?当然不可以!
你可以创建一个全局异常拦截器 ,统一返回给前端的格式,参考:
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler public SaResult handlerException (Exception e ) { e.printStackTrace (); return SaResult .error (e.getMessage ()); } }
权限通配符 Sa-Token允许你根据通配符指定泛权;
例如当一个账号拥有art.*的权限时,art.add、art.delete、art.update都将匹配通过
StpUtil.hasPermission("art.add" ); StpUtil.hasPermission("art.update" ); StpUtil.hasPermission("goods.add" ); StpUtil.hasPermission("art.delete" ); StpUtil.hasPermission("user.delete" ); StpUtil.hasPermission("user.update" ); StpUtil.hasPermission("index.js" ); StpUtil.hasPermission("index.css" ); StpUtil.hasPermission("index.html" );
注解鉴权 常用注解 尽管使用代码鉴权非常方便,但是我仍希望把鉴权逻辑和业务逻辑分离开来,我可以使用注解鉴权吗?当然可以!注解鉴权 —— 优雅的将鉴权与业务代码分离 !
@SaCheckLogin: 登录校验 —— 只有登录之后才能进入该方法。
@SaCheckRole("admin"): 角色校验 —— 必须具有指定角色标识才能进入该方法。
@SaCheckPermission("user:add"): 权限校验 —— 必须具有指定权限才能进入该方法。
@SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法。
@SaCheckHttpBasic: HttpBasic校验 —— 只有通过 HttpBasic 认证后才能进入该方法。
@SaCheckHttpDigest: HttpDigest校验 —— 只有通过 HttpDigest 认证后才能进入该方法。
@SaCheckDisable("comment"):账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。
@SaCheckSign:API 签名校验 —— 用于跨系统的 API 签名参数校验。
@SaIgnore:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。
以上的注解,只能标注在 Controller 方法上
配置拦截器 @Configuration public class SaTokenConfigure implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry ) { registry.addInterceptor (new SaInterceptor ()).addPathPatterns ("/**" ); } }
路由器鉴权:了解 https://sa-token.cc/doc.html#/use/route-check
注册 Sa-Token 路由拦截器 需求场景: 项目中所有接口均需要登录认证,只有 “登录接口” 本身对外开放;
怎么实现呢?给每个接口加上鉴权注解?手写全局拦截器?似乎都不是非常方便
@Configuration public class SaTokenConfigure implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry ) { registry.addInterceptor (new SaInterceptor (handle -> StpUtil .checkLogin ())) .addPathPatterns ("/**" ) .excludePathPatterns ("/user/doLogin" ); } }
更完整的写法 @Configuration public class SaTokenConfigure implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry ) { registry.addInterceptor (new SaInterceptor (handler -> { SaRouter .match ("/**" , "/user/doLogin" , r -> StpUtil .checkLogin ()); 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 ("/**" ); } }
AOP鉴权 在 注解式鉴权 章节,我们非常轻松的实现了注解鉴权, 但是默认的拦截器模式却有一个缺点,那就是无法在 **Controller层**以外的代码使用进行校验
因此Sa-Token供AOP插件,你只需在pom.xml里添加如下依赖,便可以在任意层级使用注解鉴权
<dependency > <groupId > cn.dev33</groupId > <artifactId > sa-token-spring-aop</artifactId > <version > 1.44.0</version > </dependency >
使用拦截器模式,只能把注解写在Controller层,使用AOP模式,可以将注解写在任意层级
拦截器模式和AOP模式不可同时集成 ,否则会在Controller层发生一个注解校验两次的bug
网关统一鉴权:了解 引入依赖
注:Redis包是必须的,因为我们需要和各个服务通过Redis来同步数据
<dependency > <groupId > cn.dev33</groupId > <artifactId > sa-token-reactor-spring-boot3-starter</artifactId > <version > 1.44.0</version > </dependency > <dependency > <groupId > cn.dev33</groupId > <artifactId > sa-token-redis-jackson</artifactId > <version > 1.44.0</version > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-pool2</artifactId > </dependency >
实现鉴权接口 关于数据的获取,建议以下方案三选一 :
在网关处集成SQL框架 ,直接从数据库查询数据
先从Redis中获取数据 ,获取不到时走SQL框架查询数据库
先从Redis中获取缓存数据 ,获取不到时走RPC调用子服务 (专门的权限数据 提供服务) 获取
@Component public class StpInterfaceImpl implements StpInterface { @Override public List <String > getPermissionList (Object loginId, String loginType ) { return ...; } @Override public List <String > getRoleList (Object loginId, String loginType ) { return ...; } }
注册全局过滤器 @Configuration public class SaTokenConfigure { @Bean public SaReactorFilter getSaReactorFilter ( ) { return new SaReactorFilter () .addInclude ("/**" ) .addExclude ("/favicon.ico" ) .setAuth (obj -> { SaRouter .match ("/**" , "/user/doLogin" , r -> StpUtil .checkLogin ()); 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" )); }) .setError (e -> { return SaResult .error (e.getMessage ()); }) ; } }
常用类 StpUtil:鉴权工具类 https://sa-token.cc/doc.html#/api/stp-util
SaSession:会话对象 SaSession session = StpUtil.getSession();
https://sa-token.cc/doc.html#/api/sa-session
基本使用:https://sa-token.cc/doc.html#/use/session
全局类、方法 SaManager SaManager 负责管理 Sa-Token 所有全局组件。
SaManager.getConfig(); SaManager.getSaTokenDao(); SaManager.getStpInterface(); SaManager.getSaTokenContext(); SaManager.getSaTokenListener(); SaManager.getSaTemp(); SaManager.getSaJsonTemplate(); SaManager.getSaSignTemplate(); SaManager.getStpLogic("type" ); SaManager.getStpLogic("type" , false ); SaManager.putStpLogic(stpLogic);
SaHolder Sa-Token上下文持有类,通过此类快速获取当前环境的相关对象
SaHolder.getContext(); SaHolder.getRequest(); SaHolder.getResponse(); SaHolder.getStorage(); SaHolder.getApplication();