Sa-Token (java权限认证框架)

系列文章目录


系列文章目录

前言

一、SaToken简介

[SpringBoot 集成 Sa-Token](#SpringBoot 集成 Sa-Token)

1、添加依赖

2、设置配置文件

3、启动项目

二、基础功能

1.登录认证

会话查询

token查询

2.权限认证

权限校验

角色校验

3.踢人下线

​编辑

强制注销

踢人下线

顶人下线

4.注解鉴权

拦截器注解鉴权

AOP注解鉴权(另外一种注解鉴权方式)

注意:

5.路由拦截鉴权

[校验规则 SaRouter.match()](#校验规则 SaRouter.match())

提前退出匹配链(SaRouter.stop())

[打开一个独立的作用域(free() )](#打开一个独立的作用域(free() ))

使用注解忽略掉路由拦截校验

关闭注解校验

6.Session会话

Account-Session

Token-Session

Custom-Session

在Session上存取值

[SaSession 与 HttpSession的区别](#SaSession 与 HttpSession的区别)

7.框架配置

配置方式

1、在application.yml配置

2、通过代码配置

三、深入提升

1.Sa-Token集成Redis

2.前后端分离 (无Cookie模式)

3.自定义Token风格

自定义Token生成策略

4.Token提交前缀

5.同端互斥登录

1、在yml中添加如下配置:

2、调用登录等相关接口时声明设备类型

6.记住我模式

实现原理

亮点:前后端分离模式下如何实现[记住我]?

登录时指定Token有效期

7.登录参数&注销参数

1、登录参数

2、注销参数

3、遍历登录终端详细操作

8.二级认证

指定业务标识进行二级认证

使用注解进行二级认证

9.模拟他人&身份切换

操作其它账号的api

临时身份切换

10.账号封禁

1、账号封禁

2、分类封禁

3、阶梯封禁

4、使用注解完成封禁校验

5、封禁信息持久化

11.密码加密(了解即可)

常见的加密算法

对称加密AES加密

非对称加密RSA加密(已过时)

Base64编码与解码

Base32编码与解码

TOTP验证器

BCrypt加密

12.会话查询

1、单账号会话查询

2、全部会话检索

参数详解:

[13.Http Basic 认证](#13.Http Basic 认证)

[1、启用Http Basic 认证](#1、启用Http Basic 认证)

2、其它启用方式

3、URL认证

[4、Http Digest认证](#4、Http Digest认证)

14.全局侦听器

1、工作原理

2、自定义侦听器实现

全局异常拦截器

15.全局过滤器

在SpringBoot中注册过滤器

自定义过滤器执行顺序

在WebFlux中注册过滤器

16.多账号认证

Kit模式

在多账户模式下使用注解鉴权

同端多登陆

运行时不可更改LoginType

总结



前言

本文是博主参考官方文档所记录自己的学习笔记,主打一个学习,Sa-Token框架确实好用,很强,官方文档写的也很细致,非常适合我这种正在学习阶段的小白!!!


提示:以下是本篇文章正文内容,下面案例可供参考

一、SaToken简介

Sa-Token是一个轻量级Java权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。

SaToken官方文档:https://sa-token.cc/

SpringBoot 集成 Sa-Token

1、添加依赖

在项目中添加依赖:

XML 复制代码
        <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot3-starter</artifactId>
            <version>1.44.0</version>
        </dependency>
        <!-- Sa-Token 整合 SpringAOP 实现注解鉴权 -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-aop</artifactId>
            <version>1.44.0</version>
        </dependency>

2、设置配置文件

application.yml 文件:

XML 复制代码
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: false
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
  token-style: uuid
  # 是否输出操作日志
  is-log: true
  # 是否从 cookie中读取 token
  is-read-cookie: true

3、启动项目

java 复制代码
@SpringBootApplication
public class SaTokenDemoApplication {
    public static void main(String[] args) throws JsonProcessingException {
        SpringApplication.run(SaTokenDemoApplication.class, args);
        System.out.println("启动成功,Sa-Token 配置如下:" + SaManager.getConfig());
    }
}

二、基础功能

1.登录认证

登录访问流程:

  1. 用户提交 name + password 参数,调用登录接口。
  2. 登录成功,返回这个用户的 Token 会话凭证。
  3. 用户后续的每次请求,都携带上这个 Token。
  4. 服务器根据 Token 判断此会话是否登录成功。

会话查询

java 复制代码
// 获取当前会话账号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查询

java 复制代码
// 获取当前会话的 token 值
StpUtil.getTokenValue();

// 获取当前`StpLogic`的 token 名称
StpUtil.getTokenName();

// 获取指定 token 对应的账号id,如果未登录,则返回 null
StpUtil.getLoginIdByToken(String tokenValue);

// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
StpUtil.getTokenTimeout();

// 获取当前会话的 token 信息参数
StpUtil.getTokenInfo();

2.权限认证

核心逻辑就是判断一个账号是否拥有指定权限:

  • 有,就让你通过。
  • 没有?那么禁止访问!

深入到底层数据中,就是每个账号都会拥有一组权限码集合,框架来校验这个集合中是否包含指定的权限码。

权限校验

java 复制代码
// 获取:当前账号所拥有的权限集合
StpUtil.getPermissionList();

// 判断:当前账号是否含有指定权限, 返回 true 或 false
StpUtil.hasPermission("user.add");        

// 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException 
StpUtil.checkPermission("user.add");        

// 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get");        

// 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");    

角色校验

java 复制代码
// 获取:当前账号所拥有的角色集合
StpUtil.getRoleList();

// 判断:当前账号是否拥有指定角色, 返回 true 或 false
StpUtil.hasRole("super-admin");        

// 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
StpUtil.checkRole("super-admin");        

// 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
StpUtil.checkRoleAnd("super-admin", "shop-admin");        

// 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可] 
StpUtil.checkRoleOr("super-admin", "shop-admin");        

Sa-Token允许你根据通配符指定泛权限 ,例如当一个账号拥有art.*的权限时,art.addart.deleteart.update都将匹配通过

权限精确到按钮级的意思就是指:权限范围可以控制到页面上的每一个按钮是否显示

3.踢人下线

强制注销

java 复制代码
StpUtil.logout(10001);                    // 强制指定账号注销下线 
StpUtil.logout(10001, "PC");              // 强制指定账号指定端注销下线 
StpUtil.logoutByTokenValue("token");      // 强制指定 Token 注销下线 

踢人下线

java 复制代码
StpUtil.kickout(10001);                    // 将指定账号踢下线 
StpUtil.kickout(10001, "PC");              // 将指定账号指定端踢下线
StpUtil.kickoutByTokenValue("token");      // 将指定 Token 踢下线

强制注销 和 踢人下线 的区别在于:

  • 强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效。
  • 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线

顶人下线

"顶人下线" 操作发生在框架登录时顶退旧登录设备,属于框架内部操作,一般情形下你不会调用到此 API:

java 复制代码
StpUtil.replaced(10001);                    // 将指定账号顶下线 
StpUtil.replaced(10001, "PC");              // 将指定账号指定端顶下线
StpUtil.replacedByTokenValue("token");      // 将指定 Token 顶下线

4.注解鉴权

拦截器注解鉴权

使用前需注册拦截器

新建配置类SaTokenConfigure.java

java 复制代码
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    // 注册 Sa-Token 拦截器,打开注解式鉴权功能 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 拦截器,打开注解式鉴权功能 
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");    
    }
}
  • @SaCheckLogin: 登录校验 ------ 只有登录之后才能进入该方法。
  • @SaCheckRole("admin"): 角色校验 ------ 必须具有指定角色标识才能进入该方法。
  • @SaCheckPermission("user:add"): 权限校验 ------ 必须具有指定权限才能进入该方法。
  • @SaCheckSafe: 二级认证校验 ------ 必须二级认证之后才能进入该方法。
  • @SaCheckHttpBasic: HttpBasic校验 ------ 只有通过 HttpBasic 认证后才能进入该方法。
  • @SaCheckHttpDigest: HttpDigest校验 ------ 只有通过 HttpDigest 认证后才能进入该方法。
  • @SaCheckDisable("comment"):账号服务封禁校验 ------ 校验当前账号指定服务是否被封禁。
  • @SaCheckSign:API 签名校验 ------ 用于跨系统的 API 签名参数校验。
  • @SaIgnore:忽略校验 ------ 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。(@SaIgnore 具有最高优先级,当 @SaIgnore 和其它鉴权注解一起出现时,其它鉴权注解都将被忽略。)

注意:

@SaCheckRole@SaCheckPermission注解可设置校验模式

java 复制代码
// 注解式鉴权:只要具有其中一个权限即可通过校验 
@RequestMapping("atJurOr")
@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR)        
public SaResult atJurOr() {
    return SaResult.data("用户信息");
}

mode有两种取值:

  • SaMode.AND,标注一组权限,会话必须全部具有才可通过校验。
  • SaMode.OR,标注一组权限,会话只要具有其一即可通过校验。

角色权限双重"or校验"

java 复制代码
// 角色权限双重 "or校验":具备指定权限或者指定角色即可通过校验
@RequestMapping("userAdd")
@SaCheckPermission(value = "user.add", orRole = "admin")        
public SaResult userAdd() {
    return SaResult.data("用户信息");
}

orRole 字段代表权限校验未通过时的次要选择,两者只要其一校验成功即可进入请求方法,其有三种写法:

  • 写法一:orRole = "admin",代表需要拥有角色 admin 。
  • 写法二:orRole = {"admin", "manager", "staff"},代表具有三个角色其一即可。
  • 写法三:orRole = {"admin, manager, staff"},代表必须同时具有三个角色。

批量注解鉴权@SaCheckOr

java 复制代码
// 在 `@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(); 
}

AOP注解鉴权(另外一种注解鉴权方式)

默认的拦截器模式却有一个缺点,那就是无法在Controller层以外的代码使用进行校验

pom.xml里添加如下依赖,便可以在任意层级使用注解鉴权

XML 复制代码
<!-- Sa-Token 整合 SpringAOP 实现注解鉴权 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-aop</artifactId>
    <version>1.44.0</version>
</dependency>
注意:
  • 拦截器模式和AOP模式不可同时集成 ,否则会在Controller层发生一个注解校验两次的bug

5.路由拦截鉴权

注册Sa-Token路由拦截器

java 复制代码
@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"); 
    }
}

校验规则 SaRouter.match()

java 复制代码
@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("/**");
    }
}

提前退出匹配链(SaRouter.stop())

java 复制代码
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("/**");
  • SaRouter.back() 用于:停止匹配,结束执行,直接向前端返回结果
java 复制代码
// 执行back函数后将停止匹配,也不会进入Controller,而是直接将 back参数 作为返回值输出到前端
SaRouter.match("/user/back").back("要返回到前端的内容");

stop() 与 back() 函数的区别在于:

  • SaRouter.stop() 会停止匹配,进入Controller。
  • SaRouter.back() 会停止匹配,直接返回结果到前端。

打开一个独立的作用域(free() )

  • SaRouter.free() 打开一个独立的作用域,使内部的 stop() 不再一次性跳出整个 Auth 函数,而是仅仅跳出当前 free 作用域。
java 复制代码
// 进入 free 独立作用域 
SaRouter.match("/**").free(r -> {
    SaRouter.match("/a/**").check(/* --- */);
    SaRouter.match("/b/**").check(/* --- */).stop();
    SaRouter.match("/c/**").check(/* --- */);
});
// 执行 stop() 函数跳出 free 后继续执行下面的 match 匹配 
SaRouter.match("/**").check(/* --- */);

使用注解忽略掉路由拦截校验

@SaIgnore 注解,忽略掉路由拦截认证:

请求将会跳过拦截器的校验,直接进入 Controller 的方法中。

关闭注解校验

SaInterceptor 只要注册到项目中,默认就会打开注解校验,如果要关闭此能力,需要指定 isAnnotation 为 false:

java 复制代码
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(
        new SaInterceptor(handle -> {
            SaRouter.match("/**").check(r -> StpUtil.checkLogin());
        }).isAnnotation(false)  // 指定关闭掉注解鉴权能力,这样框架就只会做路由拦截校验了 
    ).addPathPatterns("/**");
}

setBeforeAuth 注册认证前置函数:

java 复制代码
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new SaInterceptor(handle -> {
        System.out.println(1);
    })
    .setBeforeAuth(handle -> {
        System.out.println(2);
    })
    ).addPathPatterns("/**");
}

如上代码,先执行 2,再执行注解鉴权,再执行 1,如果 beforeAuth 里包含 SaRouter.stop() 将跳过后续的注解鉴权和 auth 认证环节。

6.Session会话

Session 是会话中专业的数据缓存组件,通过 Session 我们可以很方便的缓存一些高频读写数据,提高程序性能

在 Sa-Token 中,Session 分为三种,分别是:

  • Account-Session 以账号 id 为主,只要 token 指向的账号 id 一致,那么对应的Session对象就一致
  • Token-Session 以token为主,只要token不同,那么对应的Session对象就不同
  • Custom-Session 以特定的key为主,不同key对应不同的Session对象,同样的key指向同一个Session对象

Account-Session

java 复制代码
// 获取当前账号 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");

Token-Session

java 复制代码
// 获取当前 Token 的 Token-Session 对象
StpUtil.getTokenSession();

// 获取指定 Token 的 Token-Session 对象
StpUtil.getTokenSessionByToken(token);

Custom-Session

自定义 Session 指的是以一个特定的值(key)作为 SessionId 来分配的Session, 借助自定义Session,你可以为系统中的任意元素分配相应的session

例如以商品 id 作为 key 为每个商品分配一个Session,以便于缓存和商品相关的数据

java 复制代码
// 查询指定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");

在Session上存取值

java 复制代码
// 写值 
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();                         

SaSessionHttpSession的区别

  1. SaSessionHttpSession 没有任何关系,在HttpSession上写入的值,在SaSession中无法取出
  2. HttpSession并未被框架接管,在使用Sa-Token时,请在任何情况下均使用SaSession,不要使用HttpSession

注意:默认场景下,只有登录后才能通过 StpUtil.getTokenSession() 获取 Token-Session

如果想要在未登录场景下获取 Token-Session ,有两种方法:

  • 方法一:将全局配置项 tokenSessionCheckLogin 改为 false
  • 方法二:使用匿名 Token-Session

注意点:如果前端没有提交 Token ,或者提交的 Token 是一个无效 Token 的话,框架将不会根据此 Token 创建 Token-Session 对象, 而是随机一个新的 Token 值来创建 Token-Session 对象,此 Token 值可以通过 StpUtil.getTokenValue() 获取到。

7.框架配置

配置方式

1、在application.yml配置
XML 复制代码
############## 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: false
    # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
    is-share: false
    # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
    token-style: uuid
    # 是否输出操作日志 
    is-log: true
2、通过代码配置
复制代码
/**
 * Sa-Token 配置类
 */
@Configuration
public class SaTokenConfigure {
    // Sa-Token 参数配置,参考文档:https://sa-token.cc
    // 此配置会覆盖 application.yml 中的配置
    @Bean
    @Primary
    public SaTokenConfig getSaTokenConfigPrimary() {
        SaTokenConfig config = new SaTokenConfig();
        config.setTokenName("satoken");             // token 名称(同时也是 cookie 名称)
        config.setTimeout(30 * 24 * 60 * 60);       // token 有效期(单位:秒),默认30天,-1代表永不过期 
        config.setActiveTimeout(-1);              // token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
        config.setIsConcurrent(true);               // 是否允许同一账号多地同时登录(为 true 时允许一起登录,为 false 时新登录挤掉旧登录)
        config.setIsShare(false);                    // 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token,为 false 时每次登录新建一个 token)
        config.setTokenStyle("uuid");               // token 风格
        config.setIsLog(false);                     // 是否输出操作日志 
        return config;
    }
}

博主还是小白,暂时还用不上这么细致的配置,如需要详细配置请参考官方文档:

框架配置

三、深入提升

1.Sa-Token集成Redis

Sa-Token整合 RedisTemplate,导入依赖:

XML 复制代码
<!-- Sa-Token 整合 RedisTemplate -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-redis-template</artifactId>
    <version>1.44.0</version>
</dependency>

<!-- 提供 Redis 连接池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

自定义序列化方案(博主理解的不透彻,还望大佬指点迷津)

框架的默认序列化层调用为 String 序列化 -> JSON 序列化

先说较为底层的 JSON 序列化,如果你引入的是 sa-token-spring-boot-starter 集成包 (含SpringBoot3) ,那么框架将会自动引入 Jackson 框架作为 JSON 序列化方案。

注意:Sa-Token-Redis 集成包的版本尽量与 Sa-Token-Starter 集成包的版本一致,否则可能出现兼容性问题。

2.前后端分离 (无Cookie模式)

何为无Cookie模式?

无 Cookie 模式:特指不支持 Cookie 功能的终端,通俗来讲就是我们常说的 ------ 前后端分离模式

常规 Web 端鉴权方法,一般由 Cookie模式 完成,而 Cookie 有两个特性:

  1. 可由后端控制写入。
  2. 每次请求自动提交。

如何解决:

  • 不能后端控制写入了,就前端自己写入。(难点在后端如何将 Token 传递到前端
  • 每次请求不能自动提交了,那就手动提交。(难点在前端如何将 Token 传递到后端 ,同时后端将其读取出来

1、后端将token返回到前端

java 复制代码
// 登录接口
@RequestMapping("doLogin")
public SaResult doLogin() {
    // 第1步,先登录上 
    StpUtil.login(10001);
    // 第2步,获取 Token  相关参数 
    SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
    // 第3步,返回给前端 
    return SaResult.data(tokenInfo);
}

2、前端将token提交到后端

javascript 复制代码
import axios from 'axios';

// 从存储中获取token
const token = localStorage.getItem('token');

// 方法1:为单个请求设置header
axios.get('/api/user/profile', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  }
})
.then(response => console.log(response.data))
.catch(error => console.error('Error:', error));

// 方法2:设置全局请求拦截器(推荐)
axios.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 之后的所有请求都会自动携带token
axios.get('/api/user/profile')
  .then(response => console.log(response.data));

3.自定义Token风格

默认Token风格:

Sa-Token 默认的 token 生成策略是 uuid 风格,其模样类似于:623368f0-ae5e-4475-a53f-93e4225f16ae

自定义Token生成策略

java 复制代码
@Configuration
public class SaTokenConfigure {
    /**
     * 重写 Sa-Token 框架内部算法策略 
     */
    @PostConstruct
    public void rewriteSaStrategy() {
        // 重写 Token 生成策略 
        SaStrategy.instance.createToken = (loginId, loginType) -> {
            return SaFoxUtil.getRandomString(60);    // 随机60位长度字符串
        };
    }
}

4.Token提交前缀

使用需要在yml中添加如下配置:

复制代码
sa-token: 
    # 指定 token 提交时的前缀
    token-prefix: Bearer

注意:Token前缀 与 Token值 之间必须有一个空格

5.同端互斥登录

如果你经常使用腾讯QQ,就会发现它的登录有如下特点:它可以手机电脑同时在线,但是不能在两个手机上同时登录一个账号。

同端互斥登录,指的就是:像腾讯QQ一样,在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线。

1、在yml中添加如下配置:

2、调用登录等相关接口时声明设备类型

指定设备类型登录

复制代码
// 指定`账号id`和`设备类型`进行登录
StpUtil.login(10001, "PC");    

调用此方法登录后,同设备的会被顶下线(不同设备不受影响),再次访问系统时会抛出 NotLoginException 异常,场景值=-4

指定设备类型强制注销

复制代码
// 指定`账号id`和`设备类型`进行强制注销 
StpUtil.logout(10001, "PC");    

如果第二个参数填写null或不填,代表将这个账号id所有在线端强制注销,被踢出者再次访问系统时会抛出 NotLoginException 异常,场景值=-2

查询当前登录的设备类型

复制代码
// 返回当前token的登录设备类型
StpUtil.getLoginDevice();    

Id反查Token

复制代码
// 获取指定loginId指定设备类型端的tokenValue 
StpUtil.getTokenValueByLoginId(10001, "APP");    

6.记住我模式

Sa-Token的登录授权,默认就是[记住我]模式 ,为了实现[非记住我]模式,你需要在登录时如下设置:

复制代码
// 设置登录账号id为10001,第二个参数指定是否为[记住我],当此值为false后,关闭浏览器后再次打开需要重新登录
StpUtil.login(10001, false);

实现原理

Cookie作为浏览器提供的默认会话跟踪机制,其生命周期有两种形式,分别是:

  • 临时Cookie:有效期为本次会话,只要关闭浏览器窗口,Cookie就会消失。
  • 持久Cookie:有效期为一个具体的时间,在时间未到期之前,即使用户关闭了浏览器Cookie也不会消失。

利用Cookie的此特性,我们便可以轻松实现 [记住我] 模式:

  • 勾选 [记住我] 按钮时:调用StpUtil.login(10001, true),在浏览器写入一个持久Cookie储存 Token,此时用户即使重启浏览器 Token 依然有效。
  • 不勾选 [记住我] 按钮时:调用StpUtil.login(10001, false),在浏览器写入一个临时Cookie储存 Token,此时用户在重启浏览器后 Token 便会消失,导致会话失效。

亮点:前后端分离模式下如何实现[记住我]?

在PC浏览器环境下进行前后端分离模式开发

XML 复制代码
// 前端代码 - 需要手动处理登录响应
axios.post('/login', { username, password, rememberMe: true })
  .then(response => {
    const token = response.data.token;
    
    if (rememberMe) {
      // "记住我":使用 localStorage(持久存储)
      localStorage.setItem("satoken", token);
    } else {
      // 不"记住我":使用 sessionStorage(临时存储)
      sessionStorage.setItem("satoken", token);
    }
    
    // 将 token 设置到请求头中
    axios.defaults.headers.common['Authorization'] = token;
  });

登录时指定Token有效期

java 复制代码
// 示例1:
// 指定token有效期(单位: 秒),如下所示token七天有效
StpUtil.login(10001, new SaLoginParameter().setTimeout(60 * 60 * 24 * 7));

// ----------------------- 示例2:所有参数
// `SaLoginParameter`为登录参数Model,其有诸多参数决定登录时的各种逻辑,例如:
StpUtil.login(10001, new SaLoginParameter()
            .setDevice("PC")                // 此次登录的客户端设备类型, 用于[同端互斥登录]时指定此次登录的设备类型
            .setIsLastingCookie(true)        // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
            .setTimeout(60 * 60 * 24 * 7)    // 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的 timeout 值)
            .setToken("xxxx-xxxx-xxxx-xxxx") // 预定此次登录的生成的Token 
            .setIsWriteHeader(false)         // 是否在登录后将 Token 写入到响应头
            );

7.登录参数&注销参数

1、登录参数

java 复制代码
StpUtil.login(10001, new SaLoginParameter()
        .setDeviceType("PC")             // 此次登录的客户端设备类型, 一般用于完成 [同端互斥登录] 功能
        .setDeviceId("xxxxxxxxx")        // 此次登录的客户端设备ID, 登录成功后该设备将标记为可信任设备
        .setIsLastingCookie(true)        // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在)
        .setTimeout(60 * 60 * 24 * 7)    // 指定此次登录 token 的有效期, 单位:秒,-1=永久有效
        .setActiveTimeout(60 * 60 * 24 * 7) // 指定此次登录 token 的最低活跃频率, 单位:秒,-1=不进行活跃检查
        .setIsConcurrent(true)           // 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
        .setIsShare(false)                // 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token)
        .setMaxLoginCount(12)            // 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
        .setMaxTryTimes(12)              // 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
        .setExtra("key", "value")        // 记录在 Token 上的扩展参数(只在 jwt 模式下生效)
        .setToken("xxxx-xxxx-xxxx-xxxx") // 预定此次登录的生成的Token 
        .setIsWriteHeader(false)         // 是否在登录后将 Token 写入到响应头
        .setTerminalExtra("key", "value")// 本次登录挂载到 SaTerminalInfo 的自定义扩展数据
        .setReplacedRange(SaReplacedRange.CURR_DEVICE_TYPE) // 顶人下线的范围: CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端
        .setOverflowLogoutMode(SaLogoutMode.LOGOUT)         // 溢出 maxLoginCount 的客户端,将以何种方式注销下线: LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线
        .setRightNowCreateTokenSession(true)                // 是否立即创建对应的 Token-Session (true=在登录时立即创建,false=在第一次调用 getTokenSession() 时创建)
        .setupCookieConfig(cookie->{     // 设置 Cookie 配置项 
            cookie.setDomain("sa-token.cc");  // 设置:作用域
            cookie.setPath("/shop");          // 设置:路径 (一般只有当你在一个域名下部署多个项目时才会用到此值。)
            cookie.setSecure(true);           // 设置:是否只在 https 协议下有效
            cookie.setHttpOnly(true);         // 设置:是否禁止 js 操作 Cookie 
            cookie.setSameSite("Lax");        // 设置:第三方限制级别(Strict=完全禁止,Lax=部分允许,None=不限制)
            cookie.addExtraAttr("aa", "bb");  // 设置:额外扩展属性
        }
);

2、注销参数

java 复制代码
// 当前客户端注销 
StpUtil.logout(new SaLogoutParameter()
        // 注销范围: TOKEN=只注销当前 token 的会话,ACCOUNT=注销当前 token 指向的 loginId 其所有客户端会话
        // 此参数只在调用 StpUtil.logout() 时有效
        .setRange(SaLogoutRange.TOKEN)   
);

// 指定 token 注销
StpUtil.logoutByTokenValue("xxxxxxxxxxxxxxxxxxxxxxx", new SaLogoutParameter()
        // 如果 token 已被冻结,是否保留其操作权 (是否允许此 token 调用注销API)(默认 false)
        // 此参数只在调用 StpUtil.[logout/kickout/replaced]ByTokenValue("token") 时有效
        .setIsKeepFreezeOps(false)  
        // 是否保留此 token 的 Token-Session 对象(默认 false)
        .setIsKeepTokenSession(true)  
);

// 指定 loginId 注销
StpUtil.logout(10001, new SaLogoutParameter()
        .setDeviceType("PC")  // 设置注销的设备类型 (如果不指定,则默认注销所有客户端)
        .setIsKeepTokenSession(true)  // 是否保留对应 token 的 Token-Session 对象(默认 false)
        .setMode(SaLogoutMode.REPLACED)  // 设置注销模式:LOGOUT=注销登录、KICKOUT=踢人下线,REPLACED=顶人下线(默认LOGOUT)
);

3、遍历登录终端详细操作

java 复制代码
// 测试 
@RequestMapping("logout")
public SaResult logout() {
    
    // 遍历账号 10001 已登录终端列表,进行详细操作
    StpUtil.forEachTerminalList(10001, (session, ter) -> {
        // 根据登录顺序,奇数的保留,偶数的下线
        if(ter.getIndex() % 2 == 0) {
            StpUtil.removeTerminalByLogout(session, ter);   // 注销下线方式 移除这个登录客户端
            // StpUtil.removeTerminalByKickout(session, ter);  // 踢人下线方式 移除这个登录客户端
            // StpUtil.removeTerminalByReplaced(session, ter);  // 顶人下线方式 移除这个登录客户端
        }
    });
    
    return SaResult.ok();
}

8.二级认证

比如代码托管平台的仓库删除操作,尽管我们已经登录了账号,当我们点击 [删除] 按钮时,还是需要再次输入一遍密码,这么做主要为了两点:

  1. 保证操作者是当前账号本人。
  2. 增加操作步骤,防止误删除重要数据。
java 复制代码
// 在当前会话 开启二级认证,时间为120秒
StpUtil.openSafe(120); 

// 获取:当前会话是否处于二级认证时间内
StpUtil.isSafe(); 

// 检查当前会话是否已通过二级认证,如未通过则抛出异常
StpUtil.checkSafe(); 

// 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)
StpUtil.getSafeTime(); 

// 在当前会话 结束二级认证
StpUtil.closeSafe(); 

指定业务标识进行二级认证

复制代码
// 在当前会话 开启二级认证,业务标识为client,时间为600秒
StpUtil.openSafe("client", 600); 

// 获取:当前会话是否已完成指定业务的二级认证 
StpUtil.isSafe("client"); 

// 校验:当前会话是否已完成指定业务的二级认证 ,如未认证则抛出异常
StpUtil.checkSafe("client"); 

// 获取当前会话指定业务二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)
StpUtil.getSafeTime("client"); 

// 在当前会话 结束指定业务标识的二级认证
StpUtil.closeSafe("client"); 

业务标识可以填写任意字符串,不同业务标识之间的认证互不影响

复制代码
// 打开了业务标识为 client 的二级认证 
StpUtil.openSafe("client"); 

// 判断是否处于 shop 的二级认证,会返回 false 
StpUtil.isSafe("shop");  // 返回 false 

// 也不会通过校验,会抛出异常 
StpUtil.checkSafe("shop"); 

使用注解进行二级认证

复制代码
// 二级认证:必须二级认证之后才能进入该方法 
@SaCheckSafe      
@RequestMapping("add")
public String add() {
    return "用户增加";
}

// 指定业务类型,进行二级认证校验
@SaCheckSafe("art")
@RequestMapping("add2")
public String add2() {
    return "文章增加";
}

9.模拟他人&身份切换

操作其它账号的api

复制代码
// 获取指定账号10001的`tokenValue`值 
StpUtil.getTokenValueByLoginId(10001);

// 将账号10001的会话注销登录
StpUtil.logout(10001);

// 获取账号10001的Session对象, 如果session尚未创建, 则新建并返回
StpUtil.getSessionByLoginId(10001);

// 获取账号10001的Session对象, 如果session尚未创建, 则返回null 
StpUtil.getSessionByLoginId(10001, false);

// 获取账号10001是否含有指定角色标识 
StpUtil.hasRole(10001, "super-admin");

// 获取账号10001是否含有指定权限码
StpUtil.hasPermission(10001, "user:add");

临时身份切换

复制代码
// 将当前会话[身份临时切换]为其它账号(本次请求内有效)
StpUtil.switchTo(10044);

// 此时再调用此方法会返回 10044 (我们临时切换到的账号id)
StpUtil.getLoginId();

// 结束 [身份临时切换]
StpUtil.endSwitch();

System.out.println("------- [身份临时切换]调用开始...");
StpUtil.switchTo(10044, () -> {
    System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch());  // 输出 true
    System.out.println("获取当前登录账号id: " + StpUtil.getLoginId());   // 输出 10044
});
System.out.println("------- [身份临时切换]调用结束...");

10.账号封禁

1、账号封禁

java 复制代码
// 先踢下线
StpUtil.kickout(10001); 
// 再封禁账号
StpUtil.disable(10001, 86400); 

// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
StpUtil.checkDisable(10001); 

// 通过校验后,再进行登录:
StpUtil.login(10001); 



//api:
// 封禁指定账号 
StpUtil.disable(10001, 86400); 

// 获取指定账号是否已被封禁 (true=已被封禁, false=未被封禁) 
StpUtil.isDisable(10001); 

// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
StpUtil.checkDisable(10001); 

// 获取指定账号剩余封禁时间,单位:秒,如果该账号未被封禁,则返回-2 
StpUtil.getDisableTime(10001); 

// 解除封禁
StpUtil.untieDisable(10001); 

2、分类封禁

有的时候,我们并不需要将整个账号禁掉,而是只禁止其访问部分服务。

假设我们在开发一个电商系统,对于违规账号的处罚,我们设定三种分类封禁:

  • 1、封禁评价能力:账号A 因为多次虚假好评,被限制订单评价功能。
  • 2、封禁下单能力:账号B 因为多次薅羊毛,被限制下单功能。
  • 3、封禁开店能力:账号C 因为店铺销售假货,被限制开店功能。
复制代码
/*
 * 以下示例中:"comment"=评论服务标识、"place-order"=下单服务标识、"open-shop"=开店服务标识
 */

// 封禁指定用户评论能力,期限为 1天
StpUtil.disable(10001, "comment", 86400);

// 在评论接口,校验一下,会抛出异常:`DisableServiceException`,使用 e.getService() 可获取业务标识 `comment` 
StpUtil.checkDisable(10001, "comment");

// 在下单时,我们校验一下 下单能力,并不会抛出异常,因为我们没有限制其下单功能
StpUtil.checkDisable(10001, "place-order");

// 现在我们再将其下单能力封禁一下,期限为 7天 
StpUtil.disable(10001, "place-order", 86400 * 7);

// 然后在下单接口,我们添加上校验代码,此时用户便会因为下单能力被封禁而无法下单(代码抛出异常)
StpUtil.checkDisable(10001, "place-order");

// 但是此时,用户如果调用开店功能的话,还是可以通过,因为我们没有限制其开店能力 (除非我们再调用了封禁开店的代码)
StpUtil.checkDisable(10001, "open-shop");

3、阶梯封禁

对于多次违规的用户,我们常常采取阶梯处罚的策略,这种 "阶梯" 一般有两种形式:

  • 处罚时间阶梯:首次违规封禁 1 天,第二次封禁 7 天,第三次封禁 30 天,依次顺延......
  • 处罚力度阶梯:首次违规消息提醒、第二次禁言禁评论、第三次禁止账号登录,等等......

假设我们在开发一个论坛系统,对于违规账号的处罚,我们设定三种力度:

  • 1、轻度违规:封禁其发帖、评论能力,但允许其点赞、关注等操作。

  • 2、中度违规:封禁其发帖、评论、点赞、关注等一切与别人互动的能力,但允许其浏览帖子、浏览评论。

  • 3、重度违规:封禁其登录功能,限制一切能力。

    // 阶梯封禁,参数:封禁账号、封禁级别、封禁时间
    StpUtil.disableLevel(10001, 3, 10000);

    // 获取:指定账号封禁的级别 (如果此账号未被封禁则返回 -2)
    StpUtil.getDisableLevel(10001);

    // 判断:指定账号是否已被封禁到指定级别,返回 true 或 false
    StpUtil.isDisableLevel(10001, 3);

    // 校验:指定账号是否已被封禁到指定级别,如果已达到此级别(例如已被3级封禁,这里校验是否达到2级),则抛出异常 DisableServiceException
    StpUtil.checkDisableLevel(10001, 2);

如果业务足够复杂,我们还可能将 分类封禁 和 阶梯封禁 组合使用:

复制代码
// 分类阶梯封禁,参数:封禁账号、封禁服务、封禁级别、封禁时间 
StpUtil.disableLevel(10001, "comment", 3, 10000);

// 获取:指定账号的指定服务 封禁的级别 (如果此账号未被封禁则返回 -2)
StpUtil.getDisableLevel(10001, "comment");

// 判断:指定账号的指定服务 是否已被封禁到指定级别,返回 true 或 false
StpUtil.isDisableLevel(10001, "comment", 3);

// 校验:指定账号的指定服务 是否已被封禁到指定级别(例如 comment服务 已被3级封禁,这里校验是否达到2级),如果已达到此级别,则抛出异常 
StpUtil.checkDisableLevel(10001, "comment", 2);

4、使用注解完成封禁校验

使用前需注册 Sa-Token 全局拦截器

复制代码
// 校验当前账号是否被封禁,如果已被封禁会抛出异常,无法进入方法 
@SaCheckDisable
@PostMapping("send")
public SaResult send() {
    // ... 
    return SaResult.ok(); 
}

// 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法 
@SaCheckDisable("comment")
@PostMapping("send")
public SaResult send() {
    // ... 
    return SaResult.ok(); 
}

// 校验当前账号是否被封禁 comment、place-order、open-shop 等服务,指定多个值,只要有一个已被封禁,就无法进入方法 
@SaCheckDisable({"comment", "place-order", "open-shop"})
@PostMapping("send")
public SaResult send() {
    // ... 
    return SaResult.ok(); 
}

// 阶梯封禁,校验当前账号封禁等级是否达到5级,如果达到则抛出异常 
@SaCheckDisable(level = 5)
@PostMapping("send")
public SaResult send() {
    // ... 
    return SaResult.ok(); 
}

// 分类封禁 + 阶梯封禁 校验:校验当前账号的 comment 服务,封禁等级是否达到5级,如果达到则抛出异常 
@SaCheckDisable(value = "comment", level = 5)
@PostMapping("send")
public SaResult send() {
    // ... 
    return SaResult.ok(); 
}

5、封禁信息持久化

Sa-Token 默认将封禁信息储存在缓存中,缓存中的数据是"临时性的"、"易丢失的",而在大多数系统的设计中,需要将封禁数据持久化到数据库中。

Sa-Token 提供一种方案,可以在你调用 StpUtil.checkDisable(10001) 校验封禁时才会触发查询数据库 10001 账号到底有没有被封禁。 你只需要实现 StpInterfaceisDisabled 方法即可,例:

复制代码
@Component
public class StpInterfaceImpl implements StpInterface {

    /**
     * 返回指定账号 id 是否被封禁
     *
     * @param loginId  账号id
     * @param service 业务标识符
     * @return 描述该账号是否封禁的包装信息对象
     */
    public SaDisableWrapperInfo isDisabled(Object loginId, String service) {
        // 查库操作 ...  (此处仅做示例代码)
        return SaDisableWrapperInfo.createDisabled(86400, 1);
    }

}

该方法返回一个 SaDisableWrapperInfo 实例对象,用来描述指定账号是否已被封禁

复制代码
// 标准写法:new 对象返回,参数为:是否被封禁、封禁时间(秒)、封禁等级
public SaDisableWrapperInfo isDisabled(Object loginId, String service) {
    return new SaDisableWrapperInfo(true, 86400, 1);
}

// 快捷写法:被封禁,解封倒计时86400秒,封禁等级1
public SaDisableWrapperInfo isDisabled(Object loginId, String service) {
    return SaDisableWrapperInfo.createDisabled(86400, 1);
}

// 快捷写法:未被封禁 
public SaDisableWrapperInfo isDisabled(Object loginId, String service) {
    return SaDisableWrapperInfo.createNotDisabled();
}

// 快捷写法:未被封禁,且将查询结果保存到缓存中,ttl为86400,改时间内不再重复进入 isDisabled 方法 
public SaDisableWrapperInfo isDisabled(Object loginId, String service) {
    return SaDisableWrapperInfo.createNotDisabled(86400);
}

11.密码加密(了解即可)

常见的加密算法

复制代码
// md5加密 
SaSecureUtil.md5("123456");

// sha1加密 
SaSecureUtil.sha1("123456");

// sha256加密 
SaSecureUtil.sha256("123456");

对称加密

AES加密

java 复制代码
// 定义秘钥和明文
String key = "123456";
String text = "Sa-Token 一个轻量级java权限认证框架";

// 加密 
String ciphertext = SaSecureUtil.aesEncrypt(key, text);
System.out.println("AES加密后:" + ciphertext);

// 解密 
String text2 = SaSecureUtil.aesDecrypt(key, ciphertext);
System.out.println("AES解密后:" + text2);

附:内部密钥生成策略

java 复制代码
    private static SecretKeySpec getSecretKey(final String password) throws NoSuchAlgorithmException {
        KeyGenerator kg = KeyGenerator.getInstance("AES");
        //获取SHA1PRNG伪随机数生成器
        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
        //将实际密码作为伪随机数生成器的种子
        random.setSeed(password.getBytes());
        //利用伪随机数生成器生成128位的密钥,能确保解密时生成的密钥的一致性
        kg.init(128, random);
        SecretKey secretKey = kg.generateKey();
        return new SecretKeySpec(secretKey.getEncoded(), "AES");
    }

非对称加密

RSA加密(已过时)

复制代码
// 定义私钥和公钥 
String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAO+wmt01pwm9lHMdq7A8gkEigk0XKMfjv+4IjAFhWCSiTeP7dtlnceFJbkWxvbc7Qo3fCOpwmfcskwUc3VSgyiJkNJDs9ivPbvlt8IU2bZ+PBDxYxSCJFrgouVOpAr8ar/b6gNuYTi1vt3FkGtSjACFb002/68RKUTye8/tdcVilAgMBAAECgYA1COmrSqTUJeuD8Su9ChZ0HROhxR8T45PjMmbwIz7ilDsR1+E7R4VOKPZKW4Kz2VvnklMhtJqMs4MwXWunvxAaUFzQTTg2Fu/WU8Y9ha14OaWZABfChMZlpkmpJW9arKmI22ZuxCEsFGxghTiJQ3tK8npj5IZq5vk+6mFHQ6aJAQJBAPghz91Dpuj+0bOUfOUmzi22obWCBncAD/0CqCLnJlpfOoa9bOcXSusGuSPuKy5KiGyblHMgKI6bq7gcM2DWrGUCQQD3SkOcmia2s/6i7DUEzMKaB0bkkX4Ela/xrfV+A3GzTPv9bIBamu0VIHznuiZbeNeyw7sVo4/GTItq/zn2QJdBAkEA8xHsVoyXTVeShaDIWJKTFyT5dJ1TR++/udKIcuiNIap34tZdgGPI+EM1yoTduBM7YWlnGwA9urW0mj7F9e9WIQJAFjxqSfmeg40512KP/ed/lCQVXtYqU7U2BfBTg8pBfhLtEcOg4wTNTroGITwe2NjL5HovJ2n2sqkNXEio6Ji0QQJAFLW1Kt80qypMqot+mHhS+0KfdOpaKeMWMSR4Ij5VfE63WzETEeWAMQESxzhavN1WOTb3/p6icgcVbgPQBaWhGg==";
String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvsJrdNacJvZRzHauwPIJBIoJNFyjH47/uCIwBYVgkok3j+3bZZ3HhSW5Fsb23O0KN3wjqcJn3LJMFHN1UoMoiZDSQ7PYrz275bfCFNm2fjwQ8WMUgiRa4KLlTqQK/Gq/2+oDbmE4tb7dxZBrUowAhW9NNv+vESlE8nvP7XXFYpQIDAQAB";

// 文本
String text = "Sa-Token 一个轻量级java权限认证框架";

// 使用公钥加密
String ciphertext = SaSecureUtil.rsaEncryptByPublic(publicKey, text);
System.out.println("公钥加密后:" + ciphertext);

// 使用私钥解密
String text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext);
System.out.println("私钥解密后:" + text2); 

Base64编码与解码

java 复制代码
// 文本
String text = "Sa-Token 一个轻量级java权限认证框架";

// 使用Base64编码
String base64Text = SaBase64Util.encode(text);
System.out.println("Base64编码后:" + base64Text);

// 使用Base64解码
String text2 = SaBase64Util.decode(base64Text);
System.out.println("Base64解码后:" + text2); 

Base32编码与解码

java 复制代码
// 文本
String text = "Sa-Token 一个轻量级java权限认证框架";

// 使用Base32编码
String base32Text = SaBase32Util.encode(text);
System.out.println("Base32编码后:" + base32Text);

// 使用Base32解码
String text2 = SaBase32Util.decode(base32Text);
System.out.println("Base32解码后:" + text2); 

TOTP验证器

java 复制代码
// 1、生成密钥
String secretKey = SaTotpUtil.generateSecretKey();
System.out.println("TOTP 秘钥: " + secretKey);

// 2、生成扫码字符串
String qeString = SaTotpUtil.generateGoogleSecretKey("zhangsan", secretKey);
System.out.println("扫码字符串: " + qeString);

// 3、计算当前 TOTP 码
String code = SaTotpUtil.generateTOTP(secretKey);
System.out.println("当前时间戳对应的 TOTP 码: " + code);

// 4、验证用户输入
boolean isValid = SaTotpUtil.validateTOTP(secretKey, code, 1);
System.out.println("验证结果: " + isValid);

BCrypt加密

java 复制代码
// 使用方法
String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt()); 

// 使用checkpw方法检查被加密的字符串是否与原始字符串匹配:
BCrypt.checkpw(candidate_password, stored_hash); 

// gensalt方法提供了可选参数 (log_rounds) 来定义加盐多少,也决定了加密的复杂度:
String strong_salt = BCrypt.gensalt(10);
String stronger_salt = BCrypt.gensalt(12); 

12.会话查询

1、单账号会话查询

StpUtil.getTerminalListByLoginId( loginId ) 可获取指定账号已登录终端列表信息

复制代码
public static void main(String[] args) {
    System.out.println("账号 10001 登录设备信息:");
    List<SaTerminalInfo> terminalList = StpUtil.getTerminalListByLoginId(10001);
    for (SaTerminalInfo ter : terminalList) {
        System.out.println("登录index=" + ter.getIndex() + ", 设备type=" + ter.getDeviceType() + ", token=" + ter.getTokenValue() + ", 登录time=" + ter.getCreateTime());
    }
}

一个 SaTerminalInfo 对象代表一个终端信息

复制代码
terminal.getIndex();   // 登录会话索引值 (该账号第几个登录的设备)
terminal.getDeviceType();   // 所属设备类型,例如:PC、WEB、HD、MOBILE、APP
terminal.getTokenValue();   // 此次登录的token值
terminal.getCreateTime();   // 登录时间, 13位时间戳
terminal.getDeviceId();   // 设备id, 设备唯一标识
terminal.getExtra("key");  // 此次登录的额外自定义参数 

Extra 自定义参数可以在登录时通过如下方式指定:

复制代码
StpUtil.login(10001, new SaLoginParameter().setTerminalExtra("key", "value"));

2、全部会话检索

java 复制代码
// 查询所有已登录的 Token
StpUtil.searchTokenValue(String keyword, int start, int size, boolean sortType);

// 查询所有 Account-Session 会话
StpUtil.searchSessionId(String keyword, int start, int size, boolean sortType);

// 查询所有 Token-Session 会话
StpUtil.searchTokenSessionId(String keyword, int start, int size, boolean sortType);
参数详解:
  • keyword: 查询关键字,只有包括这个字符串的 token 值才会被查询出来。
  • start: 数据开始处索引。
  • size: 要获取的数据条数 (值为-1代表一直获取到末尾)。
  • sortType: 排序方式(true=正序:先登录的在前,false=反序:后登录的在前)。

StpUtil.searchTokenValue 和 StpUtil.searchSessionId的区别?

  • StpUtil.searchTokenValue 查询的是登录产生的所有 Token。
  • StpUtil.searchSessionId 查询的是所有已登录账号会话id。

注意:

由于会话查询底层采用了遍历方式获取数据,当数据量过大时此操作将会比较耗时,有多耗时呢?这里提供一份参考数据:

  • 单机模式下:百万会话取出10条 Token 平均耗时 0.255s
  • Redis模式下:百万会话取出10条 Token 平均耗时 3.322s

13.Http Basic 认证

1、启用Http Basic 认证

复制代码
@RequestMapping("test3")
public SaResult test3() {
    SaHttpBasicUtil.check("sa:123456");
    // ... 其它代码
    return SaResult.ok();
}

效果:当我们访问这个接口时,浏览器会强制弹出一个表单:

2、其它启用方式

java 复制代码
// 对当前会话进行 Http Basic 校验,账号密码为 yml 配置的值(例如:sa-token.http-basic=sa:123456)
SaHttpBasicUtil.check();

// 对当前会话进行 Http Basic 校验,账号密码为:`sa / 123456`
SaHttpBasicUtil.check("sa:123456");

// 以注解方式启用 Http Basic 校验
@SaCheckHttpBasic(account = "sa:123456")
@RequestMapping("test3")
public SaResult test3() {
    return SaResult.ok();
}

// 在全局拦截器 或 过滤器中启用 Basic 认证 
@Bean
public SaServletFilter getSaServletFilter() {
    return new SaServletFilter()
            .addInclude("/**").addExclude("/favicon.ico")
            .setAuth(obj -> {
                SaRouter.match("/test/**", () -> SaHttpBasicUtil.check("sa:123456"));
            });
}

3、URL认证

java 复制代码
http://sa:123456@127.0.0.1:8081/test/test3

4、Http Digest认证

复制代码
// 测试 Http Digest 认证   浏览器访问: http://localhost:8081/test/testDigest
@RequestMapping("testDigest")
public SaResult testDigest() {
    SaHttpDigestUtil.check("sa", "123456");
    return SaResult.ok();
}

// 使用注解方式开启 Http Digest 认证
@SaCheckHttpDigest("sa:123456")
@RequestMapping("testDigest2")
public SaResult testDigest() {
    return SaResult.ok();
}


// 对当前会话进行 Http Digest 校验,账号密码为 yml 配置的值(例如:sa-token.http-digest=sa:123456)
SaHttpDigestUtil.check();

url认证:

复制代码
http://sa:123456@127.0.0.1:8081/test/testDigest

14.全局侦听器

1、工作原理

Sa-Token 提供一种侦听器机制,通过注册侦听器,你可以订阅框架的一些关键性事件,例如:用户登录、退出、被踢下线等。

框架默认内置了侦听器 SaTokenListenerForLog 实现:代码参考 ,功能是控制台 log 打印输出,你可以通过配置sa-token.is-log=true开启。

2、自定义侦听器实现

2.1、新建实现类:
java 复制代码
/**
 * 自定义侦听器的实现 
 */
@Component
public class MySaTokenListener implements SaTokenListener {

    /** 每次登录时触发 */
    @Override
    public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {
        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");
    }
    
    /** 每次Token续期时触发 */
    @Override
    public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {
        System.out.println("---------- 自定义侦听器实现 doRenewTimeout");
    }

}
2.2、将侦听器注册到事件中心:

以上代码由于添加了 @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); 

快速实现一个侦听器

继承SaTokenListenerForSimple.

复制代码
@Component
public class MySaTokenListener extends SaTokenListenerForSimple {
    /*
     * SaTokenListenerForSimple 对所有事件提供了空实现,通过继承此类,你只需重写一部分方法即可实现一个可用的侦听器。
     */
    /** 每次登录时触发 */
    @Override
    public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {
        System.out.println("---------- 自定义侦听器实现 doLogin");
    }
}

使用匿名内部类的方式注册:

复制代码
// 登录时触发 
SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() {
    @Override
    public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {
        System.out.println("---------------- doLogin");
    }
});

使用try-catch包裹不安全的代码:

复制代码
// 登录时触发 
SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() {
    @Override
    public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {
        try {
            // 不安全代码需要写在 try-catch 里 
            // ......  
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
});

一个项目可以注册多个侦听器吗?

可以,多个侦听器间彼此独立,互不影响,按照注册顺序依次接受到事件通知。

全局异常拦截器

复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler
    public SaResult handlerException(Exception e) {
        e.printStackTrace(); 
        return SaResult.error(e.getMessage());
    }
}

15.全局过滤器

既然拦截器已经可以实现路由鉴权,为什么还要用过滤器再实现一遍呢?简而言之:

  1. 相比于拦截器,过滤器更加底层,执行时机更靠前,有利于防渗透扫描。
  2. 过滤器可以拦截静态资源,方便我们做一些权限控制。
  3. 部分Web框架根本就没有提供拦截器功能,但几乎所有的Web框架都会提供过滤器机制。

但是过滤器也有一些缺点,比如:

  1. 由于太过底层,导致无法率先拿到HandlerMethod对象,无法据此添加一些额外功能。
  2. 由于拦截的太全面了,导致我们需要对很多特殊路由(如/favicon.ico)做一些额外处理。
  3. 在Spring中,过滤器中抛出的异常无法进入全局@ExceptionHandler,我们必须额外编写代码进行异常处理。

在SpringBoot中注册过滤器

java 复制代码
/**
 * [Sa-Token 权限认证] 配置类 
 */
@Configuration
public class SaTokenConfigure {
    
    /**
     * 注册 [Sa-Token全局过滤器] 
     */
    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
        
                // 指定 拦截路由 与 放行路由
                .addInclude("/**").addExclude("/favicon.ico")    /* 排除掉 /favicon.ico */
                
                // 认证函数: 每次请求执行 
                .setAuth(obj -> {
                    System.out.println("---------- 进入Sa-Token全局认证 -----------");
                    
                    // 登录认证 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 
                    SaRouter.match("/**", "/user/doLogin", () -> StpUtil.checkLogin());
                    
                    // 更多拦截处理方式,请参考"路由拦截式鉴权"章节 */
                })
                
                // 异常处理函数:每次认证函数发生异常时执行此函数 
                .setError(e -> {
                    System.out.println("---------- 进入Sa-Token异常处理 -----------");
                    return SaResult.error(e.getMessage());
                })
                
                // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入)
                .setBeforeAuth(r -> {
                    // ---------- 设置一些安全响应头 ----------
                    SaHolder.getResponse()
                    // 服务器名称 
                    .setServer("sa-server")
                    // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以 
                    .setHeader("X-Frame-Options", "SAMEORIGIN")
                    // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面 
                    .setHeader("X-XSS-Protection", "1; mode=block")
                    // 禁用浏览器内容嗅探 
                    .setHeader("X-Content-Type-Options", "nosniff")
                    ;
                })
                ;
    }
    
}

改写 setError 函数的响应格式示例:

复制代码
.setError(e -> {
    // 设置响应头
    SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
    // 使用封装的 JSON 工具类转换数据格式 
    return JSONUtil.toJsonStr( SaResult.error(e.getMessage()) );
})

自定义过滤器执行顺序

SaServletFilter 默认执行顺序为 -100,如果你要自定义过滤器的执行顺序,可以使用 FilterRegistrationBean 注册

java 复制代码
/**
 * 注册 [Sa-Token 全局过滤器]
 */
@Bean
public FilterRegistrationBean<SaServletFilter> getSaServletFilter() {
    FilterRegistrationBean<SaServletFilter> frBean = new FilterRegistrationBean<>();
    frBean.setFilter(
            new SaServletFilter()
                .addInclude("/**")
                .setAuth(obj -> {
                    // ....
                })
                // 等等,其它代码 ... 
    );
    frBean.setOrder(-101);  // 更改顺序为 -101
    return frBean;
}

在 SpringBoot 中, Order 值越小,执行时机越靠前。

在WebFlux中注册过滤器

Spring WebFlux中不提供拦截器机制,因此若你的项目需要路由鉴权功能,过滤器是你唯一的选择,在Spring WebFlux注册过滤器的流程与上述流程几乎完全一致, 除了您需要将过滤器名称由SaServletFilter更换为SaReactorFilter以外,其它所有步骤均可参考以上示例。

复制代码
/**
 * [Sa-Token 权限认证] 配置类 
 */
@Configuration
public class SaTokenConfigure {
        
    /**
     * 注册 [Sa-Token全局过滤器] 
     */
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
            // 其它代码... 
        ;
    }
    
}

16.多账号认证

有的时候,我们会在一个项目中设计两套账号体系,比如一个电商系统的 user表admin表, 在这种场景下,如果两套账号我们都使用 StpUtil 类的API进行登录鉴权,那么势必会发生逻辑冲突。

在Sa-Token中,这个问题的模型叫做:多账号体系认证。

要解决这个问题,我们必须有一个合理的机制将这两套账号的授权给区分开,让它们互不干扰才行。

在构造方法时随意传入一个不同的 loginType,就可以再造一套账号登录体系。

  1. 新建一个新的权限认证类,比如: StpUserUtil.java

  2. StpUtil.java类的全部代码复制粘贴到 StpUserUtil.java里。

  3. 更改一下其 LoginType, 比如:

    public class StpUserUtil {

    复制代码
     /**
      * 账号体系标识 
      */
     public static final String TYPE = "user";    // 将 LoginType 从`login`改为`user` 
    
     // 其它代码 ... 

    }

接下来就可以像调用StpUtil.java一样调用 StpUserUtil.java了,这两套账号认证的逻辑是完全隔离的。

复制代码
// 凡是在 StpUtil 上有的方法,都可以在 StpUserUtil 上调用 
StpUserUtil.login(10001);    // 在当前会话以10001账号进行登录 
StpUserUtil.checkLogin();    // 校验当前账号是否以 User 身份进行登录 
StpUserUtil.getSession();    // 获取当前 User 账号的 Access-Session 对象 
StpUserUtil.checkPermission('xx');    // 校验当前登录的 user 账号是否具有 xx 权限 
// ...

Kit模式

建立一个 StpKit.java 门面类,声明所有的 StpLogic 引用:

复制代码
/**
 * StpLogic 门面类,管理项目中所有的 StpLogic 账号体系
 */
public class StpKit {

    /**
     * 默认原生会话对象
     */
    public static final StpLogic DEFAULT = StpUtil.stpLogic;

    /**
     * Admin 会话对象,管理 Admin 表所有账号的登录、权限认证
     */
    public static final StpLogic ADMIN = new StpLogic("admin");

    /**
     * User 会话对象,管理 User 表所有账号的登录、权限认证
     */
    public static final StpLogic USER = new StpLogic("user");

    /**
     * XX 会话对象,(项目中有多少套账号表,就声明几个 StpLogic 会话对象)
     */
    public static final StpLogic XXX = new StpLogic("xx");

}

在需要登录、权限认证的地方:

复制代码
// 在当前会话进行 Admin 账号登录
StpKit.ADMIN.login(10001);

// 在当前会话进行 User 账号登录
StpKit.USER.login(10001);

// 检测当前会话是否以 Admin 账号登录,并具有 article:add 权限
StpKit.ADMIN.checkPermission("article:add");

// 检测当前会话是否以 User 账号登录,并通过了二级认证
StpKit.USER.checkSafe();

// 获取当前 User 会话的 Session 对象,并进行写值操作 
StpKit.USER.getSession().set("name", "zhang");

在多账户模式下使用注解鉴权

框架默认的注解鉴权 如@SaCheckLogin 只针对原生StpUtil进行鉴权。

例如,我们在一个方法上加上@SaCheckLogin注解,这个注解只会放行通过StpUtil.login(id)进行登录的会话, 而对于通过StpUserUtil.login(id)进行登录的会话,则始终不会通过校验。

那么如何告诉@SaCheckLogin要鉴别的是哪套账号的登录会话呢?很简单,你只需要指定一下注解的type属性即可:

复制代码
// 通过type属性指定此注解校验的是我们自定义的`StpUserUtil`,而不是原生`StpUtil`
@SaCheckLogin(type = StpUserUtil.TYPE)
@RequestMapping("info")
public String info() {
    return "查询用户信息";
}

注:@SaCheckRole("xxx")@SaCheckPermission("xxx")同理,亦可根据type属性指定其校验的账号体系,此属性默认为"",代表使用原生StpUtil账号体系。

使用注解合并简化代码

1、重写Sa-Token默认的注解处理器:
复制代码
@Configuration
public class SaTokenConfigure {
    @PostConstruct
    public void rewriteSaStrategy() {
        // 重写Sa-Token的注解处理器,增加注解合并功能 
        SaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> {
            return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass); 
        };
    }
}
2、自定义一个注解:
复制代码
/**
 * 登录认证(User版):只有登录之后才能进入该方法 
 * <p> 可标注在函数、类上(效果等同于标注在此类的所有方法上) 
 */
@SaCheckLogin(type = "user")
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE})
public @interface SaUserCheckLogin {
    
}
3、接下来就可以使用我们的自定义注解了:
复制代码
// 使用 @SaUserCheckLogin 的效果等同于使用:@SaCheckLogin(type = "user")
@SaUserCheckLogin
@RequestMapping("info")
public String info() {
    return "查询用户信息";
}

同端多登陆

假设我们不仅需要在后台同时集成两套账号,我们还需要在一个客户端同时登陆两套账号(业务场景举例:一个APP中可以同时登陆商家账号和用户账号)。

具体表现大致为:在一个浏览器登录商家账号后,再登录用户账号,然后商家账号的登录态就会自动失效。

我们只要更改一下 StpUserUtilTokenName 即可,参考示例如下:

复制代码
public class StpUserUtil {
    
    // 使用匿名子类 重写`stpLogic对象`的一些方法 
    public static StpLogic stpLogic = new StpLogic("user") {
        // 重写 StpLogic 类下的 `splicingKeyTokenName` 函数,返回一个与 `StpUtil` 不同的token名称, 防止冲突 
        @Override
        public String splicingKeyTokenName() {
            return super.splicingKeyTokenName() + "-user";
        }
        // 同理你可以按需重写一些其它方法 ... 
    }; 
    
    // ... 
    
}

再次调用 StpUserUtil.login(10001) 进行登录授权时,token的名称将不再是 satoken,而是我们重写后的 satoken-user,这样就不会再客户端发生 token 的相互覆盖了。

不同体系不同SaTokenConfig配置

如果自定义的 StpUserUtil 需要使用不同 SaTokenConfig 对象, 也很简单,参考示例如下:

复制代码
@Configuration
public class SaTokenConfigure {
    
    @PostConstruct
    public void setSaTokenConfig() {
        // 设定 StpUtil 使用的 SaTokenConfig 配置参数对象
        SaTokenConfig config1 = new SaTokenConfig();
        config1.setTokenName("satoken1");
        config1.setTimeout(1000);
        config1.setTokenStyle("random-64");
        // 更多设置 ... 
        StpUtil.stpLogic.setConfig(config1);

        // 设定 StpUserUtil 使用的 SaTokenConfig 配置参数对象
        SaTokenConfig config2 = new SaTokenConfig();
        config2.setTokenName("satoken2");
        config2.setTimeout(2000);
        config2.setTokenStyle("tik");
        // 更多设置 ... 
        StpUserUtil.stpLogic.setConfig(config2);
    }

}

多账号体系混合鉴权

在多账号体系下,怎么在 SaInterceptor 拦截器中给一个接口登录鉴权?

复制代码
// 注册 Sa-Token 拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new SaInterceptor(handle -> {
        
        // 如果这个接口,要求客户端登录了后台 Admin 账号才能访问:
        SaRouter.match("/art/getInfo").check(r -> StpUtil.checkLogin());

        // 如果这个接口,要求客户端登录了前台 User 账号才能访问:
        SaRouter.match("/art/getInfo").check(r -> StpUserUtil.checkLogin());
        
        // 如果这个接口,要求客户端同时登录 Admin 和 User 账号,才能访问:
        SaRouter.match("/art/getInfo").check(r -> {
            StpUtil.checkLogin();
            StpUserUtil.checkLogin();
        });

        // 如果这个接口,要求客户端登录 Admin 和 User 账号任意一个,就能访问:
        SaRouter.match("/art/getInfo").check(r -> {
            if(StpUtil.isLogin() == false && StpUserUtil.isLogin() == false) {
                throw new SaTokenException("请登录后再访问接口");
            }
        });
        
    })).addPathPatterns("/**");
}

在一个接口里获取是哪个体系的账号正在登录

可以分别用两个体系的 isLogin() 方法去判断,哪个返回 true 就代表正在登录哪个体系

java 复制代码
@RequestMapping("test")
public SaResult test2() {
    
    String loginType = "";
    
    if(StpUtil.isLogin()) {
        loginType = StpUtil.getLoginType();
    }
    if(StpUserUtil.isLogin()) {
        loginType = StpUserUtil.getLoginType();
    }
    
    System.out.println("当前登录的 loginType:" + loginType);

    return SaResult.ok();
}

请注意此处可能出现的两种边际情况:

  • 两个 if 均返回 false:代表客户端在两个账号体系都没有登录。
  • 两个 if 均返回 true:代表客户端在两个账号体系都登录了。

运行时不可更改LoginType

java 复制代码
StpUtil.login(10001);
StpUtil.getStpLogic().setLoginType("user");
StpUtil.getSession().set("name", "zhangsan");

这是一种错误写法:LoginType 不可在运行时更改,只能在项目启动时指定。一旦项目启动成功后再修改 LoginType ,就会造成线程安全问题和严重的逻辑问题。


总结

记住我模式在前后端分离开发时,会有安全风险

Sa-Token 目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。

  • 登录认证 ------ 单端登录、多端登录、同端互斥登录、七天内免登录。
  • 权限认证 ------ 权限认证、角色认证、会话二级认证。
  • 踢人下线 ------ 根据账号id踢人下线、根据Token值踢人下线。
  • 注解式鉴权 ------ 优雅的将鉴权与业务代码分离。
  • 路由拦截式鉴权 ------ 根据路由拦截鉴权,可适配 restful 模式。
  • Session会话 ------ 全端共享Session,单端独享Session,自定义Session,方便的存取值。
  • 持久层扩展 ------ 可集成 Redis,重启数据不丢失。
  • 前后台分离 ------ APP、小程序等不支持 Cookie 的终端也可以轻松鉴权。
  • Token风格定制 ------ 内置六种 Token 风格,还可:自定义 Token 生成策略。
  • 记住我模式 ------ 适配 [记住我] 模式,重启浏览器免验证。
  • 二级认证 ------ 在已登录的基础上再次认证,保证安全性。
  • 模拟他人账号 ------ 实时操作任意用户状态数据。
  • 临时身份切换 ------ 将会话身份临时切换为其它账号。
  • 同端互斥登录 ------ 像QQ一样手机电脑同时在线,但是两个手机上互斥登录。
  • 账号封禁 ------ 登录封禁、按照业务分类封禁、按照处罚阶梯封禁。
  • 密码加密 ------ 提供基础加密算法,可快速 MD5、SHA1、SHA256、AES 加密。
  • 会话查询 ------ 提供方便灵活的会话查询接口。
  • Http Basic认证 ------ 一行代码接入 Http Basic、Digest 认证。
  • 全局侦听器 ------ 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。
  • 全局过滤器 ------ 方便的处理跨域,全局设置安全响应头等操作。
  • 多账号体系认证 ------ 一个系统多套账号分开鉴权(比如商城的 User 表和 Admin 表)
  • 单点登录 ------ 内置三种单点登录模式:同域、跨域、同Redis、跨Redis、前后端分离等架构都可以搞定。
  • 单点注销 ------ 任意子系统内发起注销,即可全端下线。
  • OAuth2.0认证 ------ 轻松搭建 OAuth2.0 服务,支持openid模式 。
  • 分布式会话 ------ 提供共享数据中心分布式会话方案。
  • 微服务网关鉴权 ------ 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证。
  • RPC调用鉴权 ------ 网关转发鉴权,RPC调用鉴权,让服务调用不再裸奔
  • 临时Token认证 ------ 解决短时间的 Token 授权问题。
  • 独立Redis ------ 将权限缓存与业务缓存分离。
  • Quick快速登录认证 ------ 为项目零代码注入一个登录页面。
  • 标签方言 ------ 提供 Thymeleaf 标签方言集成包,提供 beetl 集成示例。
  • jwt集成 ------ 提供三种模式的 jwt 集成方案,提供 token 扩展参数能力。
  • RPC调用状态传递 ------ 提供 dubbo、grpc 等集成包,在RPC调用时登录状态不丢失。
  • 参数签名 ------ 提供跨系统API调用签名校验模块,防参数篡改,防请求重放。
  • 自动续签 ------ 提供两种Token过期策略,灵活搭配使用,还可自动续签。
  • 开箱即用 ------ 提供SpringMVC、WebFlux、Solon 等常见框架集成包,开箱即用。
  • 最新技术栈 ------ 适配最新技术栈:支持 SpringBoot 3.x,jdk 17。
相关推荐
头发那是一根不剩了39 分钟前
Spring Boot「多数据源并存」的设计思路,它与动态数据源又有什么区别?
java·spring boot·后端
谢景行^顾39 分钟前
numpy
开发语言·python·numpy
o***592743 分钟前
spring注入static属性
java·后端·spring
风象南1 小时前
Spring Boot实现HTTPS双向认证
java·spring boot·后端
敲代码的瓦龙1 小时前
操作系统相关的一些问题总结
linux·c语言·开发语言
青春不流名1 小时前
Java List初始化的例子
java·windows·list
4***17271 小时前
【MySQL篇】使用Java操作MySQL实现数据交互
java·mysql·交互
牛奔1 小时前
php 8.2 配置安装php-zbarcode扩展
android·开发语言·php
sheji34161 小时前
【开题答辩全过程】以 基于Spring Boot的流浪动物救助系统设计为例,包含答辩的问题和答案
java·spring boot·后端