修改若依的数据权限功能

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 修改若依的数据权限功能

若依(cloud版本)的数据权限功能是通过注解实现的,在需要数据权限的方法上加上注解。

在注解中判断当前用户的角色对应的数据权限类型,在执行的sql语句后面拼接部门和用户的sql过滤条件从而实现数据权限功能。

数据权限的核心功能的代码如下:如果当前用户有多个角色,那么多个角色之间不同的数据权限通过sql的or来连接。

最后在mybatis的xml中通过拼接sql语句实现。

这个功能没有问题,使用$来拼接sql也没有问题,因为数据权限中需要拼接的语句是写死的,而且若依也做了防注入的操作。

695f5fd483454696a688532caf6ed971.png


但是我们的客户验收项目的时候会做代码扫描,这种通过$来拼接sql语句的情况是不允许出现的,哪怕这么写完全没有注入风险也不可以。


所以要找到另一种方法替换若依当前的数据权限功能。

网上翻了翻其他的成品框架,决定升级mybatis为myabtis-plus,使用mybatis-plus的内置拦截器来实现。数据权限的核心功能。


写一个自定义拦截器,实现myabtis-plus的内置拦截器InnerInterceptor ,就可以在sql语句执行前拦截到sql相关信息 ,在这里进行数据权限的操作。


先上拦截器的参考代码:

@Component
public class DataScopeInnerInterceptor implements InnerInterceptor {
    @Autowired
    private RedisService redisService;
    @Autowired
    private DataScopeService dataScopeService;//这个service的功能是使用feign调用auth模块中的代码,也就是下面代码的getUserDataScope方法。在登录的时候获得当前数据权限的角色id和部门id
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
                            ResultHandler resultHandler, BoundSql boundSql) {
        String originalSql = boundSql.getSql();
        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        //带有datascope注解的mapper方法名
        Object cacheObject1 = redisService.getCacheObject(UserConstants.DATA_SCOPE_ANNO);
        String scopeMethod = String.valueOf(cacheObject1);
        //获得所执行sql语句的mapper方法全限定名
        String mapperMethod = ms.getId();
        mapperMethod = mapperMethod.substring(mapperMethod.lastIndexOf(".")+1);
        if (scopeMethod.contains(mapperMethod)) {
            //获得当前登录用户
            LoginUser loginUser = SecurityUtils.getLoginUser();
            if (null != loginUser){//项目启动和登录时没有用户
                SysUser sysUser = loginUser.getSysUser();
                Object cacheObject = redisService.getCacheObject(UserConstants.USER_DATA_SCOPE + sysUser.getUserId());
                //如果redis中没有存,就再调用方法存一次
                if (null == cacheObject) {
                    dataScopeService.getUserDataScope(loginUser);
                    cacheObject = redisService.getCacheObject(UserConstants.USER_DATA_SCOPE + sysUser.getUserId());
                }
                String scoprStr = String.valueOf(cacheObject);
                if ("#".equals(scoprStr)) {//查询全部数据
                    originalSql = String.format("SELECT * FROM (%s) temp_data_scope",
                            originalSql);
                }
                else {
                    String[] split = scoprStr.split("#");
                    if (scoprStr.length()>1 && scoprStr.startsWith("#")) {   //只有对当前用户的数据权限   
                        originalSql = String.format("SELECT * FROM (%s) temp_data_scope WHERE temp_data_scope.user_id = %s", originalSql,split[1]);
                    } else if (scoprStr.length()>1 && scoprStr.endsWith("#")) {     //只有对部门的数据权限(包括本部门,本部门及子部门和自定义)  
                        originalSql = String.format("SELECT * FROM (%s) temp_data_scope WHERE temp_data_scope.dept_id IN (%s)", originalSql,split[0]);
                    } else if (!scoprStr.startsWith("#") &&  !scoprStr.endsWith("#")) {    //既有部门的数据权限又有用户的数据权限
                        originalSql = String.format("SELECT * FROM (%s) temp_data_scope WHERE temp_data_scope.dept_id IN (%s) or temp_data_scope.user_id = (%s)",originalSql, split[0],split[1]);
                    }
                }
                mpBs.sql(originalSql);
            }
        }
    }
}

这个拦截器默认拦截所有的sql语句操作,可以看到,这个是通过把原有的sql语句作为子查询来实现数据权限的,如果这样实现有以下问题需要解决:


1,子查询中的字段不能是重复的字段名,比如用户表和部门表关联,那么用户的user_id和部门的user_id不能在一起,必须有一个要起别名。所以要修改原有的语句。


2,若依之前的注解是有参数的,参数可以指定数据权限是过滤部门id还是用户id,


@DataScope(deptAlias = "d", userAlias = "u")

比如如果数据权限是“只有当前用户”的时候,按照上面的代码是在子查询外面加WHERE temp_data_scope.user_id = %s,这样就要求语句中一定要有user_id这个字段,就需要userAllias这个参数,如果没有这个参数,如果只有deptAlias参数,数据权限是不过滤的。


我打算把若依的注解的参数去掉,所以要在需要数据权限的语句中添加用户id和部门id,如果没有就关联用户表和部门表然后加上id。


3,拦截器默认拦截所有sql操作,所以要在在过滤器中找到哪些方法是加了若依之前的数据权限注解的,修改后的数据权限也是需要对加了注解的方法才进行过滤。


解决方法如下:在项目启动的时候扫描所有加注解的方法名,并redis缓存


参考代码:

@Component
public class PackageUtil {
    @Autowired
    RedisService redisService;
    @PostConstruct
    public void doTheJob(){
        getDatascopeAnnotationMethodName("com.tbenefitofcloud.system");
    }
    private void getDatascopeAnnotationMethodName(String packageName) {
        Set<Class<?>> classSet = getClassSet(packageName);
        List<String> anno = new ArrayList<>();
        for (Class<?> clazz : classSet) {
            Method[] declaredMethods = clazz.getDeclaredMethods();
            for (Method declaredMethod : declaredMethods) {
                String isNotNullStr = "";
                // 判断是否方法上存在注解  MethodInterface
                boolean annotationPresent = declaredMethod.isAnnotationPresent(DataScope.class);
                if (annotationPresent) {
                    // 获取自定义注解对象
                    DataScope methodAnno = declaredMethod.getAnnotation(DataScope.class);
                    // 根据对象获取注解值
                    anno.add(declaredMethod.getName());
                }
            }
        }
        System.out.println(anno);
        String join = StringUtils.join(anno.toArray(), ",");
        redisService.setCacheObject(UserConstants.DATA_SCOPE_ANNO,join);
    }
    /**
     * 获取类加载器
     */
    public ClassLoader getClassLoader() {
        return Thread.currentThread().getContextClassLoader();
    }
    /**
     * 加载类
     */
    public Class<?> loadClass(String className, boolean isInitialized) {
        Class<?> cls;
        try {
            cls = Class.forName(className, isInitialized, getClassLoader());
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        return cls;
    }
    /**
     * 获取指定包名下的所有类
     */
    public Set<Class<?>> getClassSet(String packageName) {
        Set<Class<?>> classSet = new HashSet<Class<?>>();
        try {
            Enumeration<URL> urls = getClassLoader().getResources(packageName.replace(".", "/"));
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                if (url != null) {
                    String protocol = url.getProtocol();
                    if (protocol.equals("file")) {
                        String packagePath = url.getPath().replaceAll("%20", " ");
                        addClass(classSet, packagePath, packageName);
                    } else if (protocol.equals("jar")) {
                        JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
                        if (jarURLConnection != null) {
                            JarFile jarFile = jarURLConnection.getJarFile();
                            if (jarFile != null) {
                                Enumeration<JarEntry> jarEntries = jarFile.entries();
                                while (jarEntries.hasMoreElements()) {
                                    JarEntry jarEntry = jarEntries.nextElement();
                                    String jarEntryName = jarEntry.getName();
                                    if (jarEntryName.endsWith(".class")) {
                                        String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replaceAll("/", ".");
                                        doAddClass(classSet, className);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return classSet;
    }
    private void addClass(Set<Class<?>> classSet, String packagePath, String packageName) {
        File[] files = new File(packagePath).listFiles(new FileFilter() {
            public boolean accept(File file) {
                return (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory();
            }
        });
        for (File file : files) {
            String fileName = file.getName();
            if (file.isFile()) {
                String className = fileName.substring(0, fileName.lastIndexOf("."));
                if (StringUtil.isNotEmpty(packageName)) {
                    className = packageName + "." + className;
                }
                doAddClass(classSet, className);
            } else {
                String subPackagePath = fileName;
                if (StringUtil.isNotEmpty(packagePath)) {
                    subPackagePath = packagePath + "/" + subPackagePath;
                }
                String subPackageName = fileName;
                if (StringUtil.isNotEmpty(packageName)) {
                    subPackageName = packageName + "." + subPackageName;
                }
                addClass(classSet, subPackagePath, subPackageName);
            }
        }
    }
    private void doAddClass(Set<Class<?>> classSet, String className) {
        Class<?> cls = loadClass(className, false);
        classSet.add(cls);
    }
}

在登录的时候查找出当前用户对应角色所需的用户id和部门id,并缓存

参考代码:

public void getUserDataScope(LoginUser userInfo) {
        SysUser sysUser = userInfo.getSysUser();
        List<SysRole> roles = sysUser.getRoles();
        //该用户对应部门id
        List<Long> deptIdsByRoleId = new ArrayList<>();
        String scopeType = "";
        String userId = "";//用户id,数据权限为仅本人数据权限时使用
        for (SysRole role : roles) {
            scopeType = role.getDataScope();
            if ("2".equals(scopeType)){     //自定数据权限,从sys_role_dept表中查询
                List<Long> scopeDeptIds = remoteScopeService.getDeptIdsByRoleId(role.getRoleId());
                deptIdsByRoleId.addAll(scopeDeptIds);
            }else if ("3".equals(scopeType)){       //部门数据权限
                //当前角色对应部门id
                deptIdsByRoleId.add(sysUser.getDeptId());
            }else if ("4".equals(scopeType)){       //部门及下属部门
                List<Long> deptAndChild = remoteScopeService.findDeptAndChild(sysUser.getDeptId());
                deptIdsByRoleId.addAll(deptAndChild);
            }else if ("5".equals(scopeType)){//仅本人数据权限
                userId = sysUser.getUserId().toString();
            }
        }
        Set<Long> scopeDepts = new HashSet<>(deptIdsByRoleId);
        String join = StringUtils.join(scopeDepts.toArray(), ",");
        System.out.println(join);
        redisService.setCacheObject(UserConstants.USER_DATA_SCOPE+sysUser.getUserId(),join+"#"+userId);
    }
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
4月前
|
算法 Java Linux
Linux下文件增删改查定位压缩操作与权限所属用户
Linux下文件增删改查定位压缩操作与权限所属用户
42 0
|
4月前
|
Shell Linux 开发工具
哇~真的是你呀!今天是用户操作中的修改属性、密码设置、删除
在Linux系统中,修改属性、密码设置和删除用户都是管理用户和文件系统的常见操作,下面让我们一起来看看。
47 1
|
9月前
|
数据安全/隐私保护
fastadmin是如何设置没有权限的用户不能访问某些页面的?
fastadmin是如何设置没有权限的用户不能访问某些页面的?
247 0
|
2月前
|
关系型数据库 MySQL 数据库
MySQL技能完整学习列表9、用户管理和权限控制——1、创建和管理用户——2、权限授予和撤销
MySQL技能完整学习列表9、用户管理和权限控制——1、创建和管理用户——2、权限授予和撤销
44 0
|
6月前
|
UED
路由权限登录后还保留上一个登录角色的权限,刷新一下就好了的解决方案
路由权限登录后还保留上一个登录角色的权限,刷新一下就好了的解决方案
31 0
|
数据安全/隐私保护
如何检查某个用户是否具有某个权限对象上定义的某种权限
比如下图这个ABAP development studi里创建的角色是为了实现CDS view DEMO_CDS_AUTH_LIT_PFCG的权限控制,里面使用到了权限对象S_CARRID.
104 0
如何检查某个用户是否具有某个权限对象上定义的某种权限
|
SQL 存储 数据库
数据权限这样设计,你觉得如何?
在项目实际开发中我们不光要控制一个用户能访问哪些资源,还需要控制用户只能访问资源中的某部分数据。 控制一个用户能访问哪些资源我们有很成熟的权限管理模型即RBAC,但是控制用户只能访问某部分资源(即我们常说的数据权限)使用RBAC模型是不够的,本文我们尝试在RBAC模型的基础上融入数据权限的管理控制。
2789 1
|
安全
【自然框架】 权限 的视频演示(二): 权限到字段、权限到记录
继续。这里演示权限到字段和权限到记录。            权限到字段有两种安全级别,      1、低安全级别。有些项目不需要做到控制每一个字段是否显示,那么就可以采用这种级别。低安全级别就是:如果一个节点里面没有设置可以访问哪些字段,那么就默认为不需要做到控制字段的程度,就是说节点里的字段都是可以访问的。
1213 0