SpringBoot集成Sa-Token进行鉴权

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-adduser-deleteuser-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环境隔离说明

有同学经常会把 SaSessionHttpSession 进行混淆,例如:

@PostMapping("/resetPoints")
public void reset(HttpSession session) {
    // 在HttpSession上写入一个值 
    session.setAttribute("name", 66);
    // 在SaSession进行取值
    System.out.println(StpUtil.getSession().getAttribute("name"));    // 输出null
}

要点:

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

9 框架配置

框架配置

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 {
    /**
     * 重写 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

注意

  1. Token前缀 与 Token值 之间必须有一个空格。
  2. 一旦配置了 Token前缀,则前端提交token时,必须带有前缀,否则会导致框架无法读取token。
  3. 由于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 二级认证

在某些敏感操作下,我们需要对已登录的会话进行二次验证

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

  1. 保证操作者是当前账号本人
  2. 增加操作步骤,防止误删除重要数据

这就是我们本篇要讲的 -------- 二级认证,即:在已登录会话的基础上,进行再次验证,提高会话的安全性。

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

参数详解

  • 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) {
        // ... 
    }

}

22 全局过滤器

全局过滤器

23 多账号认证

多账号认证

未完待续…

SpringBoot中整合sa-token,可以按照以下步骤进行操作。 1. 添加依赖:在`pom.xml`文件中添加sa-token的Redis集成包依赖。可以使用官方提供的Redis集成包`sa-token-dao-redis-jackson`,具体依赖如下: ``` <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-dao-redis-jackson</artifactId> <version>1.34.0</version> </dependency> ``` 2. 配置sa-token:在SpringBoot的配置文件中,配置sa-token的相关属性,包括Redis连接信息、token有效期等。可以参考sa-token的官方文档进行配置。 3. 注解:在需要进行的方法上添加相应的注解。例如,使用`@SaCheckLogin`注解表示该方法需要登录认证,使用`@SaCheckRole`注解表示该方法需要具有指定角色才能访问。可以根据具体需求选择合适的注解进行。 4. 注册拦截器:在高版本的SpringBoot中(≥2.6.x),需要额外添加`@EnableWebMvc`注解才能使注册拦截器生效。可以在配置类上添加该注解。 通过以上步骤,就可以在SpringBoot中成功整合sa-token,并实现基于注解的功能。请根据具体需求进行配置和使用。 #### 引用[.reference_title] - *1* [【SaToken使用】SpringBoot整合SaToken(一)token自动续期+token定期刷新+注解](https://blog.csdn.net/weixin_43165220/article/details/126889045)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [springboot:整合sa-token](https://blog.csdn.net/weixin_43296313/article/details/124274443)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Spring Boot中使用Sa-Token实现轻量级登录与](https://blog.csdn.net/m0_71777195/article/details/129175616)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

直达CPU

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值