基于Sa-Token构建权限系统

基于Sa-Token构建权限系统实战

  1. 首先引入Sa-Token的两个依赖
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
</dependency>
<!-- Sa-Token 整合 jwt -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-jwt</artifactId>
</dependency>

  1. 在application.yml可以进行配置,常见配置如token名字、token有效期、是否允许并发登录、token前缀、jwt密钥等。
# Sa-Token配置
sa-token:
  # token名称 (同时也是cookie名称)
  token-name: Authorization
  # token有效期 设为一天 (必定过期) 单位: 秒
  timeout: 86400
  # token最低活跃时间 (指定时间无操作就过期) 单位: 秒
  active-timeout: 1800
  # 允许动态设置 token 有效期
  dynamic-active-timeout: true
  # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
  is-share: false
  # 是否尝试从header里读取token
  is-read-header: true
  # 是否尝试从cookie里读取token
  is-read-cookie: false
  # token前缀
  token-prefix: "Bearer"
  # jwt秘钥
  jwt-secret-key: abcdefghijklmnopqrstuvwxyz

  1. 自定义配置类:SaTokenConfig
    作为一个权限认证框架,肯定是要实现拦截器的功能的。因此我们的配置类需要实现WebMvcConfigurer,用于添加拦截器。
    在配置类中,我们做四件事情,分别是:添加拦截器SaInterceptor、注入StpLogicJwtForSimple实现JWT模式、注入权限接口实现SaPermissionImpl,注入使用Redis实现的自定义DAO层。

  2. 添加拦截器
    重写void addInterceptors(InterceptorRegistry registry),添加拦截器SaInterceptor。
    逻辑大致如下:

通过AllUrlHandler,可以拿到所有url路径。
使用SaRouter路由匹配操作工具类,调用match传入拦截的URL列表。
链式调用check,使用Sa-Token自带的权限认证工具类StpUtil进行校验登录。
拦截器调用excludePathPatterns放行一些静态资源等排除路径。

我们自定义一个SecurityProperties,内部包含一个字符串数组,用于配置排除路径。

小插一嘴:这个AllUrlHandler参考自开源项目RuoYi-Vue-Plus,它的大致原理是通过实现InitializingBean接口,重写afterPropertiesSet方法,这个是Spring提供的扩展点,在Bean属性设置后执行,Spring MVC中的RequestMappingHandlerMapping就实现了InitializingBean接口,在afterPropertiesSet中完成了一些初始化工作,比如url和controller方法的映射。

这里AllUrlHandler就是从容器拿到RequestMappingHandlerMapping,遍历内部的RequestMappingInfo,拿到pattern并添加到集合中返回。需要稍微了解Spring Bean的生命周期,还是挺有意思的。

    /**
     * 注册sa-token的拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册路由拦截器,自定义验证规则
        registry.addInterceptor(new SaInterceptor(handler -> {
            AllUrlHandler allUrlHandler = SpringUtils.getBean(AllUrlHandler.class);
            // 登录验证 -- 排除多个路径
            SaRouter
                // 获取所有的
                .match(allUrlHandler.getUrls())
                // 对未排除的路径进行检查
                .check(() -> {
                    // 检查是否登录 是否有token
                    StpUtil.checkLogin();
                });
        })).addPathPatterns("/**")
            // 排除不需要拦截的路径
            .excludePathPatterns(securityProperties.getExcludes());
    }

注入几个Bean实现DIY注入:StpLogicJwtForSimple,整合JWT。
注入:SaPermissionImpl,实现权限管理。注入:RedisSaTokenDao,自定义DAO层存储,整合Redis。

自定义DAO层:基于Redis
SaTokenDao是Sa-Token 持久层接口,sa-token本身封装了基于内存的默认实现,因为不满足持久化的需求所以不适用。
因此,这里采用自定义持久层的方式来实现,具体实现的话,只需要实现SaTokenDao接口,重写一系列set和get方法即可。这里我选择的实现方式为基于Redission客户端封装的RedisUtils,RedisUtils是RuoYi-Vue-Plus封装的工具类,基于jackson实现序列化,覆盖了大部分Redis的使用场景。

Sa-Token如何存储token值?

这里就要介绍Sa-Token内部的几个类:

SaHolder:上下文持有类,用于快速获取SaRequest、SaResponse、SaStorage。
SaTokenContext:上下文。可以共享部分数据。
SaStorage:在一次请求的作用域内读写值,可以在不同方法间隐式传参

Sa-Token在登陆后将token存储在Storage和Dao层,采用的存储策略是多级缓存。
功能实现:登录验证
登录方法
SaLoginModel类:SaToken的登陆模型,决定登录的一些细节行为,包含设备信息、usedId等。
现在,我们从Controller层开始,解析login方法的执行流程。
Controller层:拿到username和password,调用service层的login,将token包装返回。
Service层:

根据用户名,查数据库拿到SysUser的对象。
调用checkLogin检查密码合法性。
如果没有抛出异常,构建LoginUser登录对象。
将用户信息存储入Redis和上下文,执行StpUtils.login方法。
采用异步+线程池方式记录日志。

这里我们注重关注第二步和第四步。

checkLogin(LoginType loginType, String username, Supplier supplier)

此方法记录用户失败重试次数,调用传入的supplier进行密码校验,一般来讲supplier传入BCrypt.checkpw(password, user.getPassword())比对密码和数据库内密码。每次错误都会将错误次数存入Redis,达到指定次数会直接抛出异常。
LoginUser是登录用户,内含用户基本信息、权限信息、菜单信息等。
如果执行到构建LoginUser,已经验证成功了,下一步就是如何生成token并将用户信息存储到Redis。

存储loginUser、userId到SaStorage。
构建SaLoginModel,将userId存到SaLoginModel。
调用StpUtils.login(Object id, SaLoginModel loginModel)

创建登录会话,使用StpLogicJwtForSimple分配token。
续期会话,添加token签名并设置到Redis
在Redis内写入token到loginId的映射关系,方便check的时候查找token合法性。
发布事件:登陆成功。用于监听后记录日志、实现在线用户功能等,实现切面操作。
存储:将TokenValue写入到Storage、Header。

调用StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);,将LoginUser写入SaSession缓存。

至此,登录方法分析完毕,更加深入的内容读者可以自行阅读Sa-Token源码。
SaInterceptor拦截器

checkLogin()方法

我们前面已经在SaTokenConfig配置了请求拦截器,获取所有URL并且调用checkLogin方法,现在我们深入CheckLogin方法的内部。
checkLogin方法底层调用了getLoginId获取登录会话ID,如果找不到就抛出异常。而在这个方法内部调用了getTokenValue方法,底层采用多级缓存的获取方法,先从Storage获取、然后依次从Request、Header获取。如果都获取不到,token就是null,自然无法登录。
值得注意的是,获取Token的方法并没有从DAO层获取,而是从缓存中获取。当从缓冲中得到token后,会调用getLoginIdNotHandle查找此Token对应的loginId,此时调用的是DAO层从Redis中读取,如果获取不到就证明token无效,抛出异常。
总结:调用getTokenValue从缓存拿token,调用getLoginIdNotHandle从Redis查询loginId,检查token是否有效。

SaInterceptor的preHandle方法

使用SaStrategy.instance.isAnnotationPresent.apply(method, SaIgnore.class)判断是否加了SaIgnore注解,如果加了直接返回,否则执行注解鉴权,最后调用我们传入的Lambda表达式进行鉴权拦截。
功能实现:权限功能
权限功能底层都是调用StpInterface的相关API进行获取权限,在这里我们写一个实现类重写所有方法。
主要方法包括:

获取权限列表:直接从LoginUser里面拿,我们在login的时候已经将权限信息设置进入。
获取角色列表

如何获取LoginUser

前面我们已经将LoginUser存入多个缓存,因此可以采用多级缓存的方式进行获取。
我们首先从Storage中拿到loginUser,然后从SaSession获取,底层还是先从多级缓存获取,然后最后从Dao层即Redis获取。

  1. 具体鉴权方式
// 获取:当前账号所拥有的权限集合
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");	
// 获取:当前账号所拥有的角色集合
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");		

鉴权方式2:注解(常用),以下列出了常用的注解

@SaIgnore:不验证
@SaCheckPermission("monitor:logininfor:remove"):验证权限
@SaCheckRole("super-admin")
@SaCheckDisable("comment")

Sa-Token官方文档
RuoYi-Vue-Plus

  • 26
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值