官方文档
1 添加依赖2 配置文件cn.dev33 sa-token-spring-boot-starter 1.28.0
server:
# 端口
port: 8081
# Sa-Token配置
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: false
# token风格
token-style: uuid
# 是否输出操作日志
is-log: false
3 登录认证
3.1 登录与注销
// 标记当前会话登录的账号id // 建议的参数类型:long | int | String, 不可以传入复杂类型,如:User、Admin等等 StpUtil.login(Object id); // 当前会话注销登录 StpUtil.logout(); // 获取当前会话是否已经登录,返回true=已登录,false=未登录 StpUtil.isLogin(); // 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException` StpUtil.checkLogin();3.2 会话查询
// 获取当前会话账号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对应的账号id,如果未登录,则返回 null StpUtil.getLoginIdByToken(String tokenValue); // 获取当前`StpLogic`的token名称 StpUtil.getTokenName(); // 获取当前会话的token值 StpUtil.getTokenValue(); // 获取当前会话的token信息参数 StpUtil.getTokenInfo();4 权限认证 4.1 设置权限码和角色
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Component;
import cn.dev33.satoken.stp.StpInterface;
@Component
public class StpInterfaceImpl implements StpInterface {
@Override
public List getPermissionList(Object loginId, String loginType) {
// 模拟权限码
List list = new ArrayList();
list.add("101");
list.add("user-add");
list.add("user-delete");
list.add("user-update");
list.add("user-get");
list.add("article-get");
return list;
}
@Override
public List getRoleList(Object loginId, String loginType) {
// 模拟角色
List list = new ArrayList();
list.add("admin");
list.add("super-admin");
return list;
}
}
4.2 权限认证
// 判断:当前账号是否含有指定权限, 返回true或false
StpUtil.hasPermission("user-update");
// 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException
StpUtil.checkPermission("user-update");
// 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
StpUtil.checkPermissionAnd("user-update", "user-delete");
// 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
StpUtil.checkPermissionOr("user-update", "user-delete");
4.3 角色认证
// 判断:当前账号是否拥有指定角色, 返回true或false
StpUtil.hasRole("super-admin");
// 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
StpUtil.checkRole("super-admin");
// 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
StpUtil.checkRoleAnd("super-admin", "shop-admin");
// 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
StpUtil.checkRoleOr("super-admin", "shop-admin");
4.4 权限通配符
Sa-Token允许你根据通配符指定泛权限,例如当一个账号拥有user*的权限时,user-add、user-delete、user-update都将匹配通过 。
// 当拥有 user* 权限时
StpUtil.hasPermission("user-add"); // true
StpUtil.hasPermission("user-update"); // true
StpUtil.hasPermission("art-add"); // false
// 当拥有 *-delete 权限时
StpUtil.hasPermission("user-add"); // false
StpUtil.hasPermission("user-delete"); // true
StpUtil.hasPermission("art-delete"); // true
// 当拥有 *.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 踢人下线
StpUtil.kickout(10001); // 将指定账号踢下线
StpUtil.kickout(10001, "PC"); // 将指定账号指定端踢下线
StpUtil.kickoutByTokenValue("token"); // 将指定 Token 踢下线
强制注销 和 踢人下线 的区别在于:
- 强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效。
- 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线。
对于违规账号,有时候我们仅仅将其踢下线还是远远不够的,我们还需要对其进行账号封禁防止其再次登录 。
// 封禁指定账号 // 参数一:账号id // 参数二:封禁时长,单位:秒 (86400秒=1天,此值为-1时,代表永久封禁) StpUtil.disable(10001, 86400); // 获取指定账号是否已被封禁 (true=已被封禁, false=未被封禁) StpUtil.isDisable(10001); // 获取指定账号剩余封禁时间,单位:秒 StpUtil.getDisableTime(10001); // 解除封禁 StpUtil.untieDisable(10001);
注意
对于正在登录的账号,对其账号封禁时并不会使其立刻注销
如果需要将其封禁后立即掉线,可采取先踢再封禁的策略,例如:
// 先踢下线 StpUtil.kickout(10001); // 再封禁账号 封一天1 StpUtil.disable(10001, 86400);6 注解式鉴权
注解鉴权 —— 优雅的将鉴权与业务代码分离!
- @SaCheckLogin: 登录认证 —— 只有登录之后才能进入该方法
- @SaCheckRole("admin"): 角色认证 —— 必须具有指定角色标识才能进入该方法
- @SaCheckPermission("user:add"): 权限认证 —— 必须具有指定权限才能进入该方法
- @SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法
- @SaCheckBasic: HttpBasic认证 —— 只有通过 Basic 认证后才能进入该方法
Sa-Token使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态
因此,为了使用注解鉴权,你必须手动将Sa-Token的全局拦截器注册到你项目中。
注:注册拦截器方式的注解鉴权只能加在controller层。
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册Sa-Token的注解拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关)
registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns(" );
// 根据 path 路由排除匹配
SaRouter.match(" );
// 根据请求类型匹配
SaRouter.match(SaHttpMethod.GET).check( );
// 根据一个 boolean 条件进行匹配
SaRouter.match( StpUtil.isLogin() ).check( );
// 根据一个返回 boolean 结果的lambda表达式匹配
SaRouter.match( r -> StpUtil.isLogin() ).check( );
// 多个条件一起使用
SaRouter.match(SaHttpMethod.GET).match(" );
// 可以无限连缀下去
SaRouter
.match(SaHttpMethod.GET)
.match("/admin*.js")
.notMatch("*.css")
// ....
.check( );
7.4 提前退出匹配链
使用 SaRouter.stop() 可以提前退出匹配链,例:
registry.addInterceptor(new SaRouteInterceptor((req, res, handler) -> {
SaRouter.match(");
SaRouter.match("/a).stop();
SaRouter.match("/a);
});
// 执行 stop() 函数跳出 free 后继续执行下面的 match 匹配
SaRouter.match(");
free() 的作用是:打开一个独立的作用域,使内部的 stop() 不再一次性跳出整个 Auth 函数,而是仅仅跳出当前 free 作用域。
8 Session会话Session是会话中专业的数据缓存组件,通过 Session 我们可以很方便的缓存一些高频读写数据,提高程序性能,例如:
// 在登录时缓存user对象
StpUtil.getSession().set("user", user);
// 然后我们就可以在任意处使用这个user对象
SysUser user = (SysUser) StpUtil.getSession().get("user")
在 Sa-Token 中,Session 分为三种,分别是:
- User-Session: 指的是框架为每个 账号id 分配的 Session
- Token-Session: 指的是框架为每个 token 分配的 Session
- Custom-Session: 指的是以一个 特定的值 作为SessionId,来分配的 Session
有关User-Session与Token-Session的详细区别,请参考:Session模型详解
8.1 User-Session有关账号Session的API如下:
// 获取当前账号id的Session (必须是登录后才能调用)
StpUtil.getSession();
// 获取当前账号id的Session, 并决定在Session尚未创建时,是否新建并返回
StpUtil.getSession(true);
// 获取账号id为10001的Session
StpUtil.getSessionByLoginId(10001);
// 获取账号id为10001的Session, 并决定在Session尚未创建时,是否新建并返回
StpUtil.getSessionByLoginId(10001, true);
// 获取SessionId为xxxx-xxxx的Session, 在Session尚未创建时, 返回null
StpUtil.getSessionBySessionId("xxxx-xxxx");
8.2 Token-Session
有关令牌Session的API如下:
// 获取当前token的专属Session StpUtil.getTokenSession(); // 获取指定token的专属Session StpUtil.getTokenSessionByToken(token);
在未登录状态下是否可以获取Token-Session?这取决于你配置的tokenSessionCheckLogin值是否为false,详见:框架配置
8.3 自定义Session自定义Session指的是以一个特定的值作为SessionId来分配的Session, 借助自定义Session,你可以为系统中的任意元素分配相应的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.4 Session相关操作
// 返回此Session的id
session.getId();
// 返回此Session的创建时间 (时间戳)
session.getCreateTime();
// 在Session上获取一个值
session.getAttribute('name');
// 在Session上获取一个值,并指定取不到值时返回的默认值
session.getAttribute('name', 'zhang');
// 在Session上写入一个值
session.setAttribute('name', 'zhang');
// 在Session上移除一个值
session.removeAttribute('name');
// 清空此Session的所有值
session.clearAttribute();
// 获取此Session是否含有指定key (返回true或false)
session.containsAttribute('name');
// 获取此Session会话上所有key (返回Set)
session.attributeKeys();
// 返回此Session会话上的底层数据对象(如果更新map里的值,请调用session.update()方法避免产生脏数据)
session.getDataMap();
// 将这个Session从持久库更新一下
session.update();
// 注销此Session会话 (从持久库删除此Session)
session.logout();
8.5 类型转换API
由于Session存取值默认的类型都是Object,因此我们通常会写很多不必要类型转换代码
为了简化操作,Sa-Token自v1.15.0封装了存取值API的类型转换,你可以非常方便的调用以下方法:
// 写值
session.set("name", "zhang");
// 写值 (只有在此key原本无值的时候才会写入)
session.setDefaultValue("name", "zhang");
// 取值
session.get("name");
// 取值 (指定默认值)
session.get("name", "");
// 取值 (转String类型)
session.getString("name");
// 取值 (转int类型)
session.getInt("age");
// 取值 (转long类型)
session.getLong("age");
// 取值 (转double类型)
session.getDouble("result");
// 取值 (转float类型)
session.getFloat("result");
// 取值 (指定转换类型)
session.getModel("key", Student.class);
// 取值 (指定转换类型, 并指定值为Null时返回的默认值)
session.getModel("key", Student.class, );
// 是否含有某个key
session.has("key");
8.6 Sesion环境隔离说明
有同学经常会把 SaSession 与 HttpSession 进行混淆,例如:
@PostMapping("/resetPoints")
public void reset(HttpSession session) {
// 在HttpSession上写入一个值
session.setAttribute("name", 66);
// 在SaSession进行取值
System.out.println(StpUtil.getSession().getAttribute("name")); // 输出null
}
要点:
- SaSession 与 HttpSession 没有任何关系,在HttpSession上写入的值,在SaSession中无法取出。
- HttpSession并未被框架接管,在使用Sa-Token时,请在任何情况下均使用SaSession,不要使用HttpSession。
框架配置
10 集成Redis集成Redis
11 前后端分离前后端分离
12 自定义Token风格 12.1 内置风格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__"12.2 自定义token生成策略
如果觉着以上风格都不是你喜欢的类型,那么还可以自定义token生成策略,来定制化token生成风格
怎么做呢?只需要重写 SaStrategy 策略类的 createToken 算法即可
步骤:
1、在SaTokenConfigure配置类中添加代码:
@Configuration
public class SaTokenConfigure {
@Autowired
public void rewriteSaStrategy() {
// 重写 Token 生成策略
SaStrategy.me.createToken = (loginId, loginType) -> {
return SaFoxUtil.getRandomString(60); // 随机60位长度字符串
};
}
}
2、再次调用 StpUtil.login(10001)方法进行登录,观察其生成的token样式:
gfuPSwZsnUhwgz08GTCH4wOgasWtc3odP4HLwXJ7NDGOximTvT4OlW19zeLH13 自定义Token前缀
在某些系统中,前端提交token时会在前面加个固定的前缀,例如:
{
"satoken": "Bearer xxxx-xxxx-xxxx-xxxx"
}
此时后端如果不做任何特殊处理,框架将会把Bearer视为token的一部分,无法正常读取token信息,导致鉴权失败
为此,我们需要在yml中添加如下配置:
sa-token:
# token前缀
token-prefix: Bearer
此时 Sa-Token 便可在读取 Token 时裁剪掉 Bearer,成功获取xxxx-xxxx-xxxx-xxxx
注意:
- Token前缀 与 Token值 之间必须有一个空格。
- 一旦配置了 Token前缀,则前端提交token时,必须带有前缀,否则会导致框架无法读取token。
- 由于cookie中无法存储空格字符,也就意味配置token前缀后,cookie鉴权方式将会失效,此时只能将token提交到header里进行传输。
在Sa-Token中实现记住我功能
Sa-Token的登录授权,默认就是[记住我]模式,为了实现[非记住我]模式, 你需要在登录时如下设置:
// 设置登录账号id为10001,第二个参数指定是否为[记住我],当此值为false后,关闭浏览器后再次打开需要重新登录 StpUtil.login(10001, false);14.2 实现原理
cookie作为浏览器提供的默认会话跟踪机制,其生命周期有两种形式,分别是:
- 临时cookie:有效期为本次会话,只要关闭浏览器窗口,cookie就会消失。
- 永久cookie:有效期为一个具体的时间,在时间未到期之前,即使用户关闭了浏览器cookie也不会消失。
利用cookie的此特性,我们便可以轻松实现 [记住我] 模式:
- 勾选 [记住我] 按钮时:调用StpUtil.login(10001, true),在浏览器写入一个永久cookie储存 Token,此时用户即使重启浏览器 Token 依然有效。
- 不勾选 [记住我] 按钮时:调用StpUtil.login(10001, false),在浏览器写入一个临时cookie储存 Token,此时用户在重启浏览器后 Token 便会消失,导致会话失效。
以经典跨端框架 uni-app 为例,我们可以使用如下方式达到同样的效果:
// 使用本地存储保存token,达到 [永久cookie] 的效果
uni.setStorageSync("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
// 使用globalData保存token,达到 [临时cookie] 的效果
getApp().globalData.satoken = "xxxx-xxxx-xxxx-xxxx-xxx";
如果你决定在PC浏览器环境下进行前后台分离模式开发,那么更加简单:
// 使用 localStorage 保存token,达到 [永久cookie] 的效果
localStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
// 使用 sessionStorage 保存token,达到 [临时cookie] 的效果
sessionStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
14.4 登录时指定token有效期
// 示例1:
// 指定token有效期(单位: 秒),如下所示token七天有效
StpUtil.login(10001, new SaLoginModel().setTimeout(60 * 60 * 24 * 7));
// ----------------------- 示例2:所有参数
// `SaLoginModel`为登录参数Model,其有诸多参数决定登录时的各种逻辑,例如:
StpUtil.login(10001, new SaLoginModel()
.setDevice("PC") // 此次登录的客户端设备标识, 用于[同端互斥登录]时指定此次登录的设备名称
.setIsLastingcookie(true) // 是否为持久cookie(临时cookie在浏览器关闭时会自动删除,持久cookie在重新打开后依然存在)
.setTimeout(60 * 60 * 24 * 7) // 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的timeout值)
);
15 模拟他人&身份切换
点击了解
16 同端互斥登录首先在配置文件中,将 isConcurrent 配置为false,然后调用登录等相关接口时声明设备标识即可:
-
指定设备标识登录
// 指定`账号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");
在某些敏感操作下,我们需要对已登录的会话进行二次验证
比如代码托管平台的仓库删除操作,尽管我们已经登录了账号,当我们点击 [删除] 按钮时,还是需要再次输入一遍密码,这么做主要为了两点:
- 保证操作者是当前账号本人
- 增加操作步骤,防止误删除重要数据
这就是我们本篇要讲的 -------- 二级认证,即:在已登录会话的基础上,进行再次验证,提高会话的安全性。
17.1 具体API在Sa-Token中进行二级认证非常简单,只需要使用以下API:
// 在当前会话 开启二级认证,时间为120秒 StpUtil.openSafe(120); // 获取:当前会话是否处于二级认证时间内 StpUtil.isSafe(); // 检查当前会话是否已通过二级认证,如未通过则抛出异常 StpUtil.checkSafe(); // 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证) StpUtil.getSafeTime(); // 在当前会话 结束二级认证 StpUtil.closeSafe();17.2 使用注解进行二级认证
在一个方法上使用 @SaCheckSafe 注解,可以在代码进入之前此方法之前进行一次二级认证
// 二级认证:必须二级认证之后才能进入该方法
@SaCheckSafe
@RequestMapping("add")
public String add() {
return "用户增加";
}
18 Http Basic 认证
Http Basic 是 http 协议中最基础的认证方式,其有两个特点:
- 简单、易集成。
- 功能支持度低。
在 Sa-Token 中使用 Http Basic 认证非常简单,只需调用几个简单的方法。
18.1 启用Http Basic 认证首先在一个接口中,调用 Http Basic 校验:
@RequestMapping("test3")
public SaResult test3() {
SaBasicUtil.check("sa:123456");
return SaResult.ok();
}
然后我们访问这个接口时,浏览器会强制弹出一个表单: 当我们输入账号密码后 (sa / 123456),才可以继续访问数据。
18.2 其他启用方式// 对当前会话进行 Basic 校验,账号密码为 yml 配置的值(例如:sa-token.basic=sa:123456)
SaBasicUtil.check();
// 对当前会话进行 Basic 校验,账号密码为:`sa / 123456`
SaBasicUtil.check("sa:123456");
// 以注解方式启用 Basic 校验
@SaCheckBasic(account = "sa:123456")
@RequestMapping("test3")
public SaResult test3() {
return SaResult.ok();
}
// 在全局拦截器 或 过滤器中启用 Basic 认证
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
.addInclude("
@Component
public class MySaTokenListener implements SaTokenListener {
@Override
public void doLogin(String loginType, Object loginId, SaLoginModel loginModel) {
// ...
}
@Override
public void doLogout(String loginType, Object loginId, String tokenValue) {
// ...
}
@Override
public void doKickout(String loginType, Object loginId, String tokenValue) {
// ...
}
@Override
public void doReplaced(String loginType, Object loginId, String tokenValue) {
// ...
}
@Override
public void doDisable(String loginType, Object loginId, long disableTime) {
// ...
}
@Override
public void doUntieDisable(String loginType, Object loginId) {
// ...
}
@Override
public void doCreateSession(String id) {
// ...
}
@Override
public void doLogoutSession(String id) {
// ...
}
}
22 全局过滤器
全局过滤器
23 多账号认证多账号认证
未完待续…



