相关文章推荐
爱旅游的红茶  ·  [Typescript] ...·  2 年前    · 
帅气的小熊猫  ·  淘宝开放平台 - 文档中心·  2 年前    · 
想旅行的钥匙  ·  MySQL之source命令-腾讯云开发者社 ...·  3 年前    · 
憨厚的鼠标  ·  github文件显示不全 - ...·  3 年前    · 
心软的鼠标  ·  重磅!447页《理解深度学习》新书最新版,加 ...·  3 年前    · 
Code  ›  ruoyi-common-security源码分析 - 墨天轮
string 本地线程 用户分析 关系逻辑
https://www.modb.pro/db/1762368236558028800
时尚的匕首
2 年前
  • 学习
    • 课程中心
      推荐优质内容、热门课程
    • 学习路径
      预设学习计划、达成学习目标
    • 知识图谱
      综合了解技术体系知识点
    • 课程库
      快速筛选、搜索相关课程
    • 视频学习
      专业视频分享技术知识
    • 电子文档
      快速搜索阅览技术文档
  • 文档
  • 服务
    • 服务介绍
    • 专家团队
    • 在线工具
    • 智能助手
    • 服务平台
  • 暂无图片
    • 数据库百科
    • 核心案例
    • 行业报告
    • 月度解读
    • 大事记
    • 产业图谱
  • 我的订单
  • 登录后可立即获得以下权益
    免费培训课程
    收藏优质文章
    疑难问题解答
    下载专业文档
    签到免费抽奖
    提升成长等级
    立即登录
    登录 注册
      • 登录 注册
    • 首页
    • 资讯
    • 数说
    • 活动
    • 大会
    • 课程
    • 文档
    • 排行
    • 问答
    • 我的订单
    暂无图片
    • 首页
    • 专家团队
    • 智能助手
    • 在线工具
      • SQLRUN
        在线数据库即时SQL运行平台
      • 数据库在线实训平台
        实操环境、开箱即用、一键连接
      • AWR分析
        上传AWR报告,查看分析结果
      • SQL格式化
        快速格式化绝大多数SQL语句
      • SQL审核
        审核编写规范,提升执行效率
      • PLSQL解密
        解密超4000字符的PL/SQL语句
      • OraC函数
        查询Oracle C 函数的详细描述
      • 智能助手小墨
        关于数据库相关的问题,您都可以问我
    • 精选案例
    • 新闻资讯
    • 云市场
    登录后可立即获得以下权益
    免费培训课程
    收藏优质文章
    疑难问题解答
    下载专业文档
    签到免费抽奖
    提升成长等级
    立即登录
    登录 注册
      • 登录 注册
    • 首页
    • 专家团队
    • 智能助手
    • 精选案例
    • 新闻资讯
    • 云市场
    暂无图片
    暂无图片
    暂无图片
    微信扫码
    复制链接
    新浪微博
    分享数说
    暂无图片
    采集到收藏夹
    分享到数说
    首页 / ruoyi-common-security源码分析

    ruoyi-common-security源码分析

    程序员恰恰 2024-01-14
    26


    当前前后端分离架构逐渐成为主流,尤其是以spring-cloud为代表的微服务架构流行之后,传统的spring security已经有些跟不上时代,在微服务架构中,很多第三方的开源权限框架有着简单易用,适配性好的特点,可以在其基础上进行二次开发,具有很不错的应用价值,这里选取ruoyi-security的源码进行分析,学习微服务架构的鉴权逻辑。

    1. 框架结构

      拦截器

      aop切面

      注解

      登录逻辑工具

      异常处理handler

      令牌服务

      配置


    拦截器

    HeaderInterceptor: 自定义头拦截器,将header 数据封装到线程变量中方便获得。 同时会验证当前用户有效期自动刷新有效 期

    public class HeaderInterceptor implements AsyncHandlerInterceptor
    {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
        {
            if (!(handler instanceof HandlerMethod))
            {
                return true;
            }
            
            //对应网关中的AuthFilter中将jwt令牌中的claims信息存入request header的操作,这里把用户信息从header中取出来,并且存入该线程的SecurityContextHolder
            //SecurityContextHolder内部的核心储存是一个TransmittableThreadLocal(ThreadLocal的特殊形式)
            SecurityContextHolder.setUserId(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USER_ID));
            SecurityContextHolder.setUserName(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USERNAME));
            SecurityContextHolder.setUserKey(ServletUtils.getHeader(request, SecurityConstants.USER_KEY));
            
            //这一步的底层操作,仍然是利用ServletUtils从requestHeader中获取token
            String token = SecurityUtils.getToken();
            
            //开始进入权限系统的操作流程,首先判断令牌是否为空
            if (StringUtils.isNotEmpty(token))
            {
                //利用token,解析出当前的登录用户是谁
                LoginUser loginUser = AuthUtil.getLoginUser(token);
                if (StringUtils.isNotNull(loginUser))
                {
                    //这一步是要判断该用户的登陆状态是否已经失效了
                    AuthUtil.verifyLoginUserExpire(loginUser);
                    
                    //最后把登录用户信息,存入本线程的上下文环境
                    SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser);
                }
            }
            return true;
        }
     
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
                throws Exception
        {
            SecurityContextHolder.remove();
        }
    }

    功能:由于一个用户是占用一个线程的,那么这个线程中的数据其实是这个用户的数据,或者说该用户的用户数据要存放到这跟线程中,这样在程序执行过程中,用户和线程可以近似画上等号。这样的话,会极大方便操作。

    其内部有一个较为核心的类SecurityContextHolder,这个类的作用是获取当前线程变量中的用户id,用户名称,token等信息。必须在网关通过请求头的方法传入,同时在HeaderInterceptor拦截器设置值。否则这里无法获取。上文中已经分析过是如何把请求头中的值存入本地线程的。
    public class SecurityContextHolder
    {
        private static final TransmittableThreadLocal<Map<String, Object>> THREAD_LOCAL = new TransmittableThreadLocal<>();
     
        public static void set(String key, Object value)
        
    {
            Map<String, Object> map = getLocalMap();
            map.put(key, value == null ? StringUtils.EMPTY : value);
        }
     
        public static String get(String key)
        
    {
            Map<String, Object> map = getLocalMap();
            return Convert.toStr(map.getOrDefault(key, StringUtils.EMPTY));
        }
     
        public static <T> T get(String key, Class<T> clazz)
        
    {
            Map<String, Object> map = getLocalMap();
            return StringUtils.cast(map.getOrDefault(key, null));
        }
     
        public static Map<String, Object> getLocalMap()
        
    {
            Map<String, Object> map = THREAD_LOCAL.get();
            if (map == null)
            {
                map = new ConcurrentHashMap<String, Object>();
                THREAD_LOCAL.set(map);
            }
            return map;
        }
     
        public static void setLocalMap(Map<String, Object> threadLocalMap)
        
    {
            THREAD_LOCAL.set(threadLocalMap);
        }
     
        public static Long getUserId()
        
    {
            return Convert.toLong(get(SecurityConstants.DETAILS_USER_ID), 0L);
        }
     
        public static void setUserId(String account)
        
    {
            set(SecurityConstants.DETAILS_USER_ID, account);
        }
     
        public static String getUserName()
        
    {
            return get(SecurityConstants.DETAILS_USERNAME);
        }
     
    }

    SecurityContextHolder中有一个静态的线程变量,其类型为TransmittableThreadLocal,这是阿里巴巴开源的一个类型,主要作用是处理父子线程变量不能共用的情况。ThreadLocal是跟当前线程挂钩的,所以脱离当前线程它就起不了作用。

    ThreadLocal和TransmittableThreadLocal的使用场景区别

    ThreadLocal场景:它的应用就是比如当前用户特有的一些属性,不能跟其他线程,用户共用。

    TransmittableThreadLocal场景: 就是父子线程或者不同线程需要共用一些变量。

    这个拦截器的功能并不是很复杂,主要是以下几点

    • 对应网关中的AuthFilter中将jwt令牌中的claims信息存入request header的操作,这里把用户信息从header中取出来,并且存入该线程的SecurityContextHolder

    • 获取requestHeader中的token

    • 判断令牌是否为空,解析出当前的登录用户是谁,并判断用户的token以及登录状态是否已经过期

    • 最后把用户登录信息也存入SecurityContextHolder

    以上是preHandle方法中的流程

    在afterCompletion方法中,就一件事情,清除SecurityContextHolder,这一点也很好理解,当当前用户结束操作的时候,清楚本线程中的信息,给下一个用户执行腾空间。

    切面

    • 系统中有两个aspect,分别是: InnerAuthAspect: 内部服务调用验证处理

    @Aspect
    @Component
    public class InnerAuthAspect implements Ordered
    {
        @Around("@annotation(innerAuth)")
        public Object innerAround(ProceedingJoinPoint point, InnerAuth innerAuth) throws Throwable
        
    {
            //这一步是查请求来源
            String source = ServletUtils.getRequest().getHeader(SecurityConstants.FROM_SOURCE);
            // 内部请求验证
            // 如果不是来源于内部的请求,则抛出异常
            if (!StringUtils.equals(SecurityConstants.INNER, source))
            {
                throw new InnerAuthException("没有内部访问权限,不允许访问");
            }
     
            //这里获取用户信息
            String userid = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USER_ID);
            String username = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USERNAME);
            // 用户信息验证
            // 未设置用户信息的,以及其他异常数据,同样抛出异常
            if (innerAuth.isUser() && (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)))
            {
                throw new InnerAuthException("没有设置用户信息,不允许访问 ");
            }
            //在切入点执行
            return point.proceed();
        }
     
        /**
         * 确保在权限认证aop执行前执行
         */

        @Override
        public int getOrder()
        
    {
            return Ordered.HIGHEST_PRECEDENCE + 1;
        }
    }

    这个切面实现了Ordered接口,其作用是确保在权限认证AOP执行前执行

    主逻辑方法使用@Around注解修饰,作用目标是内部注解@innerAuth

    该切面的主要功能是,

    • 验证请求是否是内部请求,

    • 验证用户信息是否设置

    这个切面很明确地说明了,对于用户请求,必须携带用户名和id,才会被视作是合法请求。

    • PreAuthorizeAspect:基于spring AOP的注解鉴权

    @Aspect
    @Component
    public class PreAuthorizeAspect
    {
        /**
         * 构建
         */

        public PreAuthorizeAspect()
        
    {
        }
     
        /**
         * 定义AOP签名 (切入所有使用鉴权注解的方法)
         */

        public  static final String POINTCUT_SIGN = " @annotation(com.ruoyi.common.security.annotation.RequiresLogin) || "
                + "@annotation(com.ruoyi.common.security.annotation.RequiresPermissions) || "
                + "@annotation(com.ruoyi.common.security.annotation.RequiresRoles)";
     
        /**
         * 声明AOP签名
         */

        @Pointcut(POINTCUT_SIGN)
        public void pointcut()
        
    {
        }
     
        /**
         * 环绕切入
         * 
         * @param joinPoint 切面对象
         * @return 底层方法执行后的返回值
         * @throws Throwable 底层方法抛出的异常
         */

        @Around("pointcut()")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable
        
    {
            // 注解鉴权
            //这一步是获取方法签名
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            //这一步的作用是方法增强,对一个Method对象进行注解检查,即判断被目标注解修饰的方法,当前请求是否有足够权限访问
            checkMethodAnnotation(signature.getMethod());
     
            try
            {
                // 执行原有逻辑
                Object obj = joinPoint.proceed();
                return obj;
            }
            catch (Throwable e)
            {
                throw e;
            }
        }
     
        /**
         * 对一个Method对象进行注解检查
         */

        public void checkMethodAnnotation(Method method)
        
    {
            // 校验 @RequiresLogin 注解
            RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class);
            if (requiresLogin != null)
            {
                AuthUtil.checkLogin();
            }
     
            // 校验 @RequiresRoles 注解
            RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);
            if (requiresRoles != null)
            {
                AuthUtil.checkRole(requiresRoles);
            }
     
            // 校验 @RequiresPermissions 注解
            RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
            if (requiresPermissions != null)
            {
                AuthUtil.checkPermi(requiresPermissions);
            }
        }
    }

    此切面的作用是提供注解鉴权

    切面中声明了AOP签名,并且使用@Pointcut注解限定了该切面的切入点。 这里使用了环绕切入。

    在around方法中提供注解鉴权:

    获取方法签名

    方法增强,对一个Method对象进行注解检查,即判断被目标注解修饰的方法,当前请求是否有足够权限访问。checkMethodAnnotation方法中会对三大注解进行校验,只要有一个不符合条件,就会抛出NotPermissionException异常。

    执行原有逻辑

    public class NotPermissionException extends RuntimeException
    {
        private static final long serialVersionUID = 1L;
     
        public NotPermissionException(String permission)
        
    {
            super(permission);
        }
     
        public NotPermissionException(String[] permissions)
        
    {
            super(StringUtils.join(permissions, ","));
        }
    }

    注解

    • 总的来看,ruoyi权限系统中的注解分为权限注解和功能注解,权限注解+aop功能增强,实现了注解鉴权,功能注解则是决定此方法是否能够被远程调用等。

    • 注解详解:

      • @RequiresLogin:登录认证:只有登录之后才能进入该方法

      • 权限注解:

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.METHOD, ElementType.TYPE })
    public @interface RequiresLogin
    {
    }

    @RequiresPermissions: 权限认证:必须具有指定权限才能进入该方法
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.METHOD, ElementType.TYPE })
    public  @interface RequiresPermissions
    {
        /**
         * 需要校验的权限码
         */

        String[] value() default {};
     
        /**
         * 验证模式:AND | OR,默认AND
         */

        Logical logical() default Logical.AND;
    }

    • 此注解有两个参数:权限码和验证逻辑

      • 权限码是一个数组,用于记录拥有哪些权限才能访问本接口

      • 验证逻辑是说明,以上的访问权限之间是或还是与的关系。

    • @RequiresRoles:角色认证:必须具有指定角色标识才能进入该方法

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.METHOD, ElementType.TYPE })
    public @interface RequiresRoles
    {
        /**
         * 需要校验的角色标识
         */

        String[] value() default {};
     
        /**
         * 验证逻辑:AND | OR,默认AND
         */

        Logical logical() default Logical.AND;
    }

    • 此注解有两个参数:角色标识和验证逻辑

      • 角色标识也是一个数组,记录访问该接口需要的角色

      • 验证逻辑:说明是或还是与的关系

    功能注解

    • @EnableCustomConfig

      • 这个注解是一个合并注解,具体作用是什么并不是很明确。但至少可以简化代码

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    // 表示通过aop框架暴露该代理对象,AopContext能够访问
    @EnableAspectJAutoProxy(exposeProxy = true)
    // 指定要扫描的Mapper类的包的路径
    @MapperScan("com.ruoyi.**.mapper")
    // 开启线程异步执行
    @EnableAsync
    // 自动加载类
    @Import({ ApplicationConfig.class, FeignAutoConfiguration.class })
    public @interface EnableCustomConfig
    {
     
    }

    • @EnableRyFeignClients

      • 这个注解是自定义feign注解,用于远程调用,添加basePackages的路径

      • 这个注解其实是对@EnableFeignClients注解的增强,给basePackages设置了默认值"com.ruoyi"

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @EnableFeignClients
    public @interface EnableRyFeignClients
    {
        String[] value() default {};
     
        String[] basePackages() default { "com.ruoyi" };
     
        Class<?>[] basePackageClasses() default {};
     
        Class<?>[] defaultConfiguration() default {};
     
        Class<?>[] clients() default {};
    }

    补充

    @EnableFeignClients 在spring cloud项目中,通过@EnableFeignClients启用feign客户端 该注解会扫描包路径下的@FeignClient注解定义的接口,并注册到IOC容器中 声明在调用服务的微服务上

    @FeignClient 在做springcloud分布式开发过程中会有需要访问其他服务的情况,每一个服务之间都是以接口的方式访问的,那么就需要使用到 @FeignClient 来访问其他服务的接口。@FeignClient 实现的是声明式的、模块化的Http客户端,可以让我们对其他服务接口的访问更边界就像是controller和service之间的调用一样。@FeignClient用于指定需要调用的目标服务 四大核心参数

     
    推荐文章
    爱旅游的红茶  ·  [Typescript] Dictionary map, filter and reduce - Zhentiw - 博客园
    2 年前
    帅气的小熊猫  ·  淘宝开放平台 - 文档中心
    2 年前
    想旅行的钥匙  ·  MySQL之source命令-腾讯云开发者社区-腾讯云
    3 年前
    憨厚的鼠标  ·  github文件显示不全 - SegmentFault 思否
    3 年前
    心软的鼠标  ·  重磅!447页《理解深度学习》新书最新版,加入扩散模型,巴斯大学教授Simon J.D. Prince撰著 - Python社区
    3 年前
    今天看啥   ·   Py中国   ·   codingpro   ·   小百科   ·   link之家   ·   卧龙AI搜索
    删除内容请联系邮箱 2879853325@qq.com
    Code - 代码工具平台
    © 2024 ~ 沪ICP备11025650号