SpringBoot集成Sa-Token进行鉴权

简介: 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中无法取出。


  1. 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值 之间必须有一个空格。


  1. 一旦配置了 Token前缀,则前端提交token时,必须带有前缀,否则会导致框架无法读取token。


  1. 由于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. 保证操作者是当前账号本人


  1. 增加操作步骤,防止误删除重要数据


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


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


22 全局过滤器


全局过滤器


23 多账号认证


多账号认证

相关文章
|
15天前
|
消息中间件 Java Kafka
Springboot集成高低版本kafka
Springboot集成高低版本kafka
|
21天前
|
NoSQL Java Redis
SpringBoot集成Redis解决表单重复提交接口幂等(亲测可用)
SpringBoot集成Redis解决表单重复提交接口幂等(亲测可用)
70 0
|
26天前
|
NoSQL Java Redis
SpringBoot集成Redis
SpringBoot集成Redis
186 0
|
1月前
|
NoSQL Java Redis
小白版的springboot中集成mqtt服务(超级无敌详细),实现不了掐我头!!!
小白版的springboot中集成mqtt服务(超级无敌详细),实现不了掐我头!!!
246 1
|
1月前
|
XML Java 关系型数据库
【SpringBoot系列】SpringBoot集成Fast Mybatis
【SpringBoot系列】SpringBoot集成Fast Mybatis
|
2月前
|
Java
【极问系列】springBoot集成elasticsearch出现Unable to parse response body for Response
【极问系列】springBoot集成elasticsearch出现Unable to parse response body for Response
|
15天前
|
SQL Java 调度
SpringBoot集成quartz定时任务trigger_state状态ERROR解决办法
SpringBoot集成quartz定时任务trigger_state状态ERROR解决办法
|
22天前
|
NoSQL Java Redis
SpringBoot集成Redis
SpringBoot集成Redis
47 1
|
26天前
|
Java 测试技术 Maven
SpringBoot集成Elasticsearch
SpringBoot集成Elasticsearch
22 0
|
1月前
|
缓存 NoSQL Java
springboot中集成redis,二次封装成工具类
springboot中集成redis,二次封装成工具类
172 0

热门文章

最新文章