Sa-Token
1 添加依赖
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.28.0</version> </dependency>
2 配置文件
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<String> getPermissionList(Object loginId, String loginType) { // 模拟权限码 List<String> list = new ArrayList<String>(); 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<String> getRoleList(Object loginId, String loginType) { // 模拟角色 List<String> list = new ArrayList<String>(); 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已被踢下线。
5.3 账号封禁
对于违规账号,有时候我们仅仅将其踢下线还是远远不够的,我们还需要对其进行账号封禁防止其再次登录 。
// 封禁指定账号 // 参数一:账号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的全局拦截器注册到你项目中。
6.1 注册拦截器
注:注册拦截器方式的注解鉴权只能加在controller层。
@Configuration public class SaTokenConfigure implements WebMvcConfigurer { // 注册Sa-Token的注解拦截器,打开注解式鉴权功能 @Override public void addInterceptors(InterceptorRegistry registry) { // 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关) registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**"); } }
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 认证:只有通过 Basic 认证后才能进入该方法 @SaCheckBasic(account = "sa:123456") @RequestMapping("add") public String add() { return "用户增加"; }
注:以上注解都可以加在类上,代表为这个类所有方法进行鉴权 。
6.3 设定校验模式
@SaCheckRole
与@SaCheckPermission
注解可设置校验模式,例如:
// 注解式鉴权:只要具有其中一个权限即可通过校验 @RequestMapping("atJurOr") @SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR) public AjaxJson atJurOr() { return AjaxJson.getSuccessData("用户信息"); }
mode有两种取值:
SaMode.AND
, 标注一组权限,会话必须全部具有才可通过校验。
SaMode.OR
, 标注一组权限,会话只要具有其一即可通过校验。
6.4 角色权限双重"or校验"
// 注解式鉴权:只要具有其中一个权限即可通过校验 @RequestMapping("userAdd") @SaCheckPermission(value = "user-add", orRole = "admin") public AjaxJson userAdd() { return AjaxJson.getSuccessData("用户信息"); }
orRole 字段代表权限认证未通过时的次要选择,两者只要其一认证成功即可通过校验,其有三种写法:
- 写法一:orRole = "admin",代表需要拥有角色 admin 。
- 写法二:orRole = {"admin", "manager", "staff"},代表具有三个角色其一即可。
- 写法三:orRole = {"admin, manager, staff"},代表必须同时具有三个角色。
6.5 在业务层使用注解鉴权
使用拦截器模式,只能在Controller层
进行注解鉴权,如需在任意层级使用注解鉴权,请参考:AOP注解鉴权
7 路由拦截鉴权
需求:项目中所有接口均需要登录认证,只有’登录接口’本身对外开放 。
7.1 注册路由拦截器
建配置类
@Configuration public class SaTokenConfigure implements WebMvcConfigurer { // 注册拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { // 注册Sa-Token的路由拦截器 registry.addInterceptor(new SaRouteInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/user/doLogin"); } }
以上代码,我们注册了一个登录认证拦截器,并且排除了/user/doLogin
接口用来开放登录(除了/user/doLogin
以外的所有接口都需要登录才能访问)。
7.2 校验函数详解
可以使用函数式编程自定义认证规则,例如:
@Configuration public class SaTokenConfigure implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 注册路由拦截器,自定义认证规则 registry.addInterceptor(new SaRouteInterceptor((req, res, handler)->{ // 根据路由划分模块,不同模块不同鉴权 SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment")); })).addPathPatterns("/**"); } }
SaRouter.match()
匹配函数有两个参数:
- 参数一:要匹配的path路由。
- 参数二:要执行的校验函数。
在校验函数内不只可以使用 StpUtil.checkPermission("xxx")
进行权限校验,你还可以写任意代码,例如:
@Configuration public class SaTokenConfigure implements WebMvcConfigurer { // 注册Sa-Token的拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { // 注册路由拦截器,自定义认证规则 registry.addInterceptor(new SaRouteInterceptor((req, res, 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,执行一个校验函数 SaRouter.match("/user/**").check(r -> StpUtil.checkLogin()); // 根据 path 路由匹配 ——— 支持写多个path,支持写 restful 风格路由 SaRouter.match("/user/**", "/goods/**", "/art/get/{id}").check( /* 要执行的校验函数 */ ); // 根据 path 路由排除匹配 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( /* 要执行的校验函数 */ ); // 多个条件一起使用 SaRouter.match(SaHttpMethod.GET).match("/**").check( /* 要执行的校验函数 */ ); // 可以无限连缀下去 SaRouter .match(SaHttpMethod.GET) .match("/admin/**") .match("/user/**") .notMatch("/**/*.js") .notMatch("/**/*.css") // .... .check( /* 只有上述所有条件都匹配成功,才会执行最后的check校验函数 */ );
7.4 提前退出匹配链
使用 SaRouter.stop()
可以提前退出匹配链,例:
registry.addInterceptor(new SaRouteInterceptor((req, res, 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")); })).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 独立作用域 SaRouter.match("/**").free(r -> { SaRouter.match("/a/**").check(/* --- */); SaRouter.match("/a/**").check(/* --- */).stop(); SaRouter.match("/a/**").check(/* --- */); }); // 执行 stop() 函数跳出 free 后继续执行下面的 match 匹配 SaRouter.match("/**").check(/* --- */);
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<String>) 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", "<defaultValue>"); // 取值 (转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, <defaultValue>); // 是否含有某个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
。
9 框架配置
10 集成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 { /** * 重写 Sa-Token 框架内部算法策略 */ @Autowired public void rewriteSaStrategy() { // 重写 Token 生成策略 SaStrategy.me.createToken = (loginId, loginType) -> { return SaFoxUtil.getRandomString(60); // 随机60位长度字符串 }; } }
2、再次调用 StpUtil.login(10001)
方法进行登录,观察其生成的token样式:
gfuPSwZsnUhwgz08GTCH4wOgasWtc3odP4HLwXJ7NDGOximTvT4OlW19zeLH
13 自定义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
里进行传输。
14 记住我模式
14.1 实现
在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 便会消失,导致会话失效。
14.3 前后台分离实现记住我
以经典跨端框架 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 二级认证
在某些敏感操作下,我们需要对已登录的会话进行二次验证
比如代码托管平台的仓库删除操作,尽管我们已经登录了账号,当我们点击 [删除] 按钮时,还是需要再次输入一遍密码,这么做主要为了两点:
- 保证操作者是当前账号本人
- 增加操作步骤,防止误删除重要数据
这就是我们本篇要讲的 -------- 二级认证,即:在已登录会话的基础上,进行再次验证,提高会话的安全性。
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("/**").addExclude("/favicon.ico") .setAuth(obj -> { SaRouter.match("/test/**", () -> SaBasicUtil.check("sa:123456")); }); }
18.3 URL认证
除了访问后再输入账号密码外,我们还可以在 URL 中直接拼接账号密码通过 Basic 认证,例如:
http://sa:123456@127.0.0.1:8081/test/test3
19 密码加密
20 会话治理
尽管框架将大部分操作提供了简易的封装,但在一些特殊场景下,我们仍需要绕过框架,直达数据底层进行一些操作。 Sa-Token提供以下API助你直接操作会话列表。
具体API
// 查询所有token StpUtil.searchTokenValue(String keyword, int start, int size); // 查询所有账号Session会话 StpUtil.searchSessionId(String keyword, int start, int size); // 查询所有令牌Session会话 StpUtil.searchTokenSessionId(String keyword, int start, int size); // 查询所有token
参数详解
keyword
: 查询关键字,只有包括这个字符串的token值才会被查询出来
start
: 数据开始处索引, 值为-1时代表一次性取出所有数据
size
: 要获取的数据条数
使用示例:
// 查询value包括1000的所有token,结果集从第0条开始,返回10条 List<String> tokenList = StpUtil.searchTokenValue("1000", 0, 10); for (String token : tokenList) { System.out.println(token); }复制到剪贴板错误复制成功12345
注意事项
由于会话查询底层采用了遍历方式获取数据,当数据量过大时此操作将会比较耗时,有多耗时呢?这里提供一份参考数据:
- 单机模式下:百万会话取出10条token平均耗时
0.255s
- Redis模式下:百万会话取出10条token平均耗时
3.322s
请根据业务实际水平合理调用API。
21 全局侦听器
接口SaTokenListener
是Sa-Token的全局侦听器,通过实现此接口,你可以在用户登陆、退出、被踢下线等关键性操作时进行一些AOP操作
框架对此侦听器的默认实现是log日志输出,你可以通过配置sa-token.is-log=true
开启
下面我们演示一下如何自定义侦听器的实现:
自定义侦听器实现
新建MySaTokenListener.java
,继承SaTokenListener
接口,并添加上注解@Component
,保证此类被SpringBoot
扫描到
/** * 自定义侦听器的实现 */ @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) { // ... } /** 每次创建Session时触发 */ @Override public void doCreateSession(String id) { // ... } /** 每次注销Session时触发 */ @Override public void doLogoutSession(String id) { // ... } }