Sa-Token | 豆包MarsCode AI刷题

81 阅读21分钟

简介

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 在背后做了大量的工作,包括但不限于:

  1. 检查此账号是否之前已有登录
  2. 为账号生成 Token 凭证与 Session 会话
  3. 记录 Token 活跃时间
  4. 通知全局侦听器,xx 账号登录成功
  5. 将 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"); //判断:当前账号是否含有指定权限,返回 truefalse

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"); //判断:当前账号是否拥有指定角色, 返回 truefalse

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);
    }
}