精华内容
下载资源
问答
  • JeeSite4.x 数据权限

    千人学习 2019-05-30 09:09:08
    免费试听地址: B站搜索JeeGit观看《JeeSite4.x数据权限教程》、《JeeSite1.2.7系列基础教程》、《JeeSite4.x系列基础教程》等相关课程!   郑重声明:购课前,请认真听完第一章 课程简介  建议实战人群...
  • 数据权限设计研究-行数据权限

    万次阅读 2019-01-17 19:22:56
    数据权限设计研究-行数据权限关于权限设计功能权限数据权限前提数据分类几种场景设计方案与思路映射表提供过滤sql的方法测试实际应用查询新增修改删除修改数据的私有,公开,部门属性私有改为部门私有改为公开部门改...

    关于权限设计

    一般来说,权限模块对于每一个系统而言都是最基础的模块,根据项目需求和功能的不同,设计方案也有许多。但从大的方面来说,可以将权限分为两大类型:功能权限数据权限

    功能权限

    主要控制不同的资源主体(用户、角色、组织等)有操作不同的资源的权限。比如常见的不同的角色能访问不同的页面(菜单权限),以及具有操作同一页面的不同功能(按钮权限)等等。对于java开发而言,功能权限的开发相对来说要简单很多,有很多现成的框架可以实现。我推荐用shiro,因为简单易用,而且能实现按钮级别的控制。

    数据权限

    主要控制不同的资源主体(用户、角色、组织等)有查看不同的数据信息的权限。数据权限又分为数据行权限和数据列权限,本篇文章主要研究一下数据行权限的控制。

    前提

    数据权限一般和业务的关系非常紧密,可能不同的业务有不同的设计方案,所以很难有一种统一而使用简单的设计方案。我的想法是:基于角色-部门的控制方式。即拥有某个角色的人,能看见当前角色所包含的部门中的数据。为了更好的设计数据权限,我总结了一下几种数据。

    数据分类

    1. 公开数据:字面意思,就是公开的数据,不需要控制数据权限。
    2. 部门数据:属于某个部门的数据,只有部门的人员可以查看。
    3. 私有数据:用户自己的数据,只能自己查看。

    几种场景

    1. 某条数据属于多个部门的情况。
    2. 某领导可以跨部门查看数据。
    3. 可以查看子部门的数据。
    4. 私有数据可以分享给别人,部门,或者公开。

    设计方案与思路

    百度上一堆关于数据权限的设计方案,基本上都是基于用户-角色-部门这个来设计,我的思路也和这个差不多,用户与数据角色挂钩,数据角色与部门挂钩,这就比直接角色与部门挂钩要相对灵活一些。虽然某个用户只能属于一个部门,但是有可能出现上面提到的第2中场景,跨部门的情况。
    在这里插入图片描述
    我的设计思路是提供一个方法,写业务的人员需要将查询的表名传给这个方法,然后我返回一段sql,这段sql只是在原来的表上进行数据权限过滤,返回的数据字段和原表一模一样,然后业务代码编写者再把这个sql作为参数传递到DAO层,拼接到FROM后面或者JOIN后面即可。这样无论是单表查询还是多表查询,都可以实现数据权限控制。
    还有一种思路是就是用mybatis拦截器去拦截sql,然后对sql进行改造拼接,但是这样我需要去拦截每一条select的sql,可能会对性能有影响。
    为了少改原有的业务表,同时统一对系统中的表进行数据权限控制,我设计了一张映射表,来映射数据表,部门,用户之间的关系。

    映射表

    映射表字段如下

    字段名类型描述备注
    ID字符串主键
    T_ID字符串数据表中的主键
    TABLE_NAME字符串数据表表名
    D_ID字符串部门ID主键
    U_ID字符串用户ID主键

    主键字段为字符串纯属个人习惯。
    有了这张映射表,我们就能根据映射表中的部门ID和用户ID是否为空来进行数据权限的识别,同时也能修改数据的所属权限(公开,部门,私有)。

    数据类型部门ID用户ID备注
    公开数据
    部门数据非空非空
    私有数据非空

    因为有私有数据分享给其他人或者部门,单条数据属于多个部门的情况,所以数据表与映射表应该是一对多的关系。

    提供过滤sql的方法

    代码如下

    /**
     * 数据权限sql拼接
     * 
     * @author chunhui.tan
     * @创建时间 2019年1月9日 下午4:30:09
     *
     */
    @Component
    public class DataSqlFilter {
    
    	@Autowired
    	private TsysUserRoleService tsysUserRoleService;
    
    	@Autowired
    	private TsysRoleDeptService tsysRoleDeptService;
    
    	@Autowired
    	private TsysDeptService tsysDeptService;
    
    	/**
    	 * 部门与数据表映射表的表名
    	 */
    	public final String MAPPING_TABLE = "DATA_MAPPING";
    
    	/**
    	 * 
    	 * @param tableName 表名
    	 * @param pkNmae    主键名
    	 * @param isPrivate 是否只获取私有
    	 * @param subDept   是否拥有子部门数据权限
    	 * @return
    	 */
    	public String getDataSql(String tableName, String pkNmae, Boolean isPrivate, Boolean subDept) {
    		// 校验参数
    		checkParam(tableName, pkNmae, isPrivate, subDept);
    		// 判断当前表是否开启数据权限
    		if (!isNeedPermissions(tableName)) {
    			return null;
    		}
    		// 获取当前用户
    		TsysUserEntity user = ShiroUtils.getUserEntity();
    		// 获取部门数据所需要的sql
    		String deptSql = getFilterSql(user, subDept);
    		StringBuilder dataSql = new StringBuilder();
    		dataSql.append("( SELECT DISTINCT S.* FROM ").append(tableName).append(" S LEFT JOIN ").append(MAPPING_TABLE)
    				.append(" T ON S.").append(pkNmae).append(" = T.T_ID AND T.TABLE_NAME= ").append("'").append(tableName)
    				.append("'");
    
    		if (isPrivate) {
    			// 只获取私有数据
    			dataSql.append(" WHERE (T.U_ID = ").append("'").append(user.getEmId()).append("'")
    					.append(" AND (T.D_ID='' OR T.D_ID IS NULL))");
    		} else {
    			// 正常数据:私有数据+部门数据+公开数据
    			dataSql.append(" WHERE T.D_ID IN ").append(deptSql).append(" OR (T.U_ID = ").append("'")
    					.append(user.getEmId()).append("'").append(" AND (T.D_ID='' OR T.D_ID IS NULL))")
    					.append(" OR ((T.D_ID='' OR T.D_ID IS NULL) AND (T.U_ID='' OR T.U_ID IS NULL))");
    		}
    		dataSql.append(")");
    		return dataSql.toString();
    
    	}
    
    	/**
    	 * 判断当前表是否开启数据权限
    	 * 
    	 * @param tableName
    	 * @return
    	 */
    	private Boolean isNeedPermissions(String tableName) {
    		// TODO 这里可以通过表名查询配置或者表来判断改表是否开启了数据权限
    		return true;
    	}
    
    	/**
    	 * 获取部门数据情况下的过滤条件SQL
    	 * 
    	 * @param user
    	 * @param subDept
    	 */
    	private String getFilterSql(TsysUserEntity user, Boolean subDept) {
    		// 部门ID列表
    		Set<String> deptIdList = new HashSet<>();
    		// 用户角色对应的部门ID列表
    		List<String> roleIdList = tsysUserRoleService.queryRoleIdList(user.getEmId());
    		if (roleIdList.size() > 0) {
    			List<String> userDeptIdList = tsysRoleDeptService
    					.queryDeptIdList(roleIdList.toArray(new String[roleIdList.size()]));
    			deptIdList.addAll(userDeptIdList);
    		}
    		// 用户子部门ID列表
    		if (subDept) {
    			List<String> subDeptIdList = tsysDeptService.getSubDeptIdList(user.getEmDeptId());
    			deptIdList.addAll(subDeptIdList);
    		}
    		List<String> result = deptIdList.stream().map(i -> {
    			return "'" + i + "'";
    		}).collect(Collectors.toList());
    		StringBuilder sqlFilter = new StringBuilder();
    		sqlFilter.append("(").append(StringUtils.join(result, ",")).append(")");
    		return sqlFilter.toString();
    	}
    
    	/**
    	 * 参数校验
    	 * 
    	 * @param tableName
    	 * @param pkNmae
    	 * @param isPrivate
    	 * @param subDept
    	 */
    	private void checkParam(String tableName, String pkNmae, Boolean isPrivate, Boolean subDept) {
    	//TODO 进行sql注入的校验
    		if (StringUtils.isBlank(tableName) || StringUtils.isBlank(pkNmae) || null == isPrivate || null == subDept) {
    			throw new XcrmsException("数据权限-缺少参数");
    		}
    	}
    }
    
    

    测试

    当前系统使用shiro做的权限,角色分成两种类型,一种是功能角色,一种是数据角色。功能角色与菜单按钮挂钩,数据角色与部门挂钩。接下来用PostMan接口测试方式来测试获取到的sql。

    只获取私有数据

    @Autowired(required = true)
    private DataSqlFilter dataSqlFilter;
    
    	/**
    	 * test
    	 * 
    	 * @param id
    	 * @return
    	 */
    	@GetMapping("/getDataSql")
    	public Result getDataSql() {
    		String dataSql = dataSqlFilter.getDataSql("PRODUCT", "P_ID", Boolean.TRUE, Boolean.FALSE);
    		System.out.println(dataSql);
    		return ResultUtil.success(dataSql);
    	}
    

    结果用Navicat处理一下:
    在这里插入图片描述
    获取普通数据
    所谓普通数据就是用户正常能看见的数据:私有数据+部门数据+公开数据。
    将参数isPrivate设置为false即可

    @Autowired(required = true)
    	private DataSqlFilter dataSqlFilter;
    
    	/**
    	 * test
    	 * 
    	 * @param id
    	 * @return
    	 */
    	@GetMapping("/getDataSql")
    	public Result getDataSql() {
    		String dataSql = dataSqlFilter.getDataSql("PRODUCT", "P_ID", Boolean.FALSE, Boolean.FALSE);
    		System.out.println(dataSql);
    		return ResultUtil.success(dataSql);
    	}
    
    

    结果用Navicat处理一下:
    在这里插入图片描述

    实际应用

    查询

    业务开发人员在查询是只需要调用这个方法获取到sql后,然后作为参数传入DAO层,在mybatis的xml文件中拼接即可(图片中的${dataSql}应为#{dataSql}),如下:
    在这里插入图片描述

    新增

    新增业务数据时,业务需要知道这个数据时那种数据类型,然后新增数据后,需要新增一条映射记录。提供过滤sql的类中可以提供统一的新增映射数据的方法。还没写,大概如下:

    	/**
    	 * 新增映射数据
    	 * 
    	 * @param pk_value  数据表数据主键值
    	 * @param tableName 数据表表名
    	 * @param type      数据类型 0 私有 1 公开 2 部门
    	 */
    	public void addMapping(String pk_value, String tableName, Integer type) {
    		// TODO 可以通过shiro获取到当前登录的用户,然后获取到用户的部门,然后根据type来新增映射关系
    
    	}
    
    

    修改

    若只是修改数据,则不关映射表的事。

    删除

    删除时需要注意,因为某条数据可能属于多个部门或者多个个人,那么当删除掉这条数据后,那么映射表中就存在多条T_ID相同和TABLE_NAME相同的数据,删除的时候应该通过T_ID和TABLE_NAME来删除映射表中的所有数据。

    修改数据的私有,公开,部门属性

    即修改映射表,按道理说,一般只会存在数据的所有人能修改,但是以防万一,业务开发人员需要判断当前数据的U_ID是否与当前用户的用户ID一致,不一致不能修改。当然,公开数据时不需要进行这一步校验的。以下修改方法都可以在DataSqlFilter类中统一提供

    私有改为部门

    即用户将私有数据分享给指定的部门
    修改方式:修改映射表中的D_ID为用户指定的部门ID

    私有改为公开

    即用户将私有数据全部公开
    修改方式:置空映射表中的U_ID和D_ID

    部门改为公开

    即将部门所拥有的数据公开
    修改方式:置空映射表中的U_ID和D_ID

    其他变更

    其他变更只要对照上面那张数据类型表即可进行修改。

    总结

    有人可能会说为什么不在原来的业务表上面加上部门ID和用户ID,从而不用映射表?
    但是这样会有一些问题,一是实际开发过程中,往往由于需求的变化,很难确定哪些表需要加部门ID和用户ID。二是这样无法满足一些特殊需求,比如:个人数据分享出去给其他人或者部门,单条数据属于多个部门。
    总结下来用以上方案来控制数据权限的优缺点如下
    优点:
    1.统一提供过滤sql,维护映射表,修改数据类型的方法,而且不用修改业务数据表,对原来的代码入侵最小化。
    2.不论是单表查询还是多表都能支持。
    3.比较灵活,针对不同需求,可以灵活调整过滤sql

    缺点:
    1.因为在对原表进行过滤时需要连接映射表,假如是多表联查,每张表都要连接映射表,可能对查询性能有影响。
    2.因为每条业务数据都会对应一条映射数据,那么意味着映射表将会有很多数据,在过滤的时候会影响性能,当然这里可以用分模块设计多个映射表的方法来解决。

    以上只是一个设计思路,还没用于实际项目,如有不足,欢迎斧正,谢谢。

    展开全文
  • Java利用Mybatis进行数据权限控制

    千次阅读 2019-06-06 18:19:18
    权限控制主要分为两块,认证(Authentication)与授权(Authorization)。认证之后确认了身份正确,业务系统就会进行授权,现在业界比较流行的模型就是RBAC(Role-Based Access Control)。RBAC包含为下面四个要素:...

    权限控制主要分为两块,认证(Authentication)与授权(Authorization)。认证之后确认了身份正确,业务系统就会进行授权,现在业界比较流行的模型就是RBAC(Role-Based Access Control)。RBAC包含为下面四个要素:用户、角色、权限、资源。用户是源头,资源是目标,用户绑定至角色,资源与权限关联,最终将角色与权限关联,就形成了比较完整灵活的权限控制模型。
    资源是最终需要控制的标的物,但是我们在一个业务系统中要将哪些元素作为待控制的资源呢?我将系统中待控制的资源分为三类:

    1. URL访问资源(接口以及网页)
    2. 界面元素资源(增删改查导入导出的按钮,重要的业务数据展示与否等)
    3. 数据资源

    现在业内普遍的实现方案实际上很粗放,就是单纯的“菜单控制”,通过菜单显示与否来达到控制权限的目的。
    我仔细分析过,现在大家做的平台分为To C和To B两种:

    1. To C一般不会有太多的复杂权限控制,甚至大部分连菜单控制都不用,全部都可以访问。
    2. To B一般都不是开放的,只要做好认证关口,能够进入系统的只有内部员工。大部分企业内部的员工互联网知识有限,而且作为内部员工不敢对系统进行破坏性的尝试。

    所以针对现在的情况,考虑成本与产出,大部分设计者也不愿意在权限上进行太多的研发力量。
    菜单和界面元素一般都是由前端编码配合存储数据实现,URL访问资源的控制也有一些框架比如SpringSecurity,Shiro。
    目前我还没有找到过数据权限控制的框架或者方法,所以自己整理了一份。

    数据权限控制原理

    数据权限控制最终的效果是会要求在同一个数据请求方法中,根据不同的权限返回不同的数据集,而且无需并且不能由研发编码控制。这样大家的第一想法应该就是AOP,拦截所有的底层方法,加入过滤条件。这样的方式兼容性较强,但是复杂程度也会更高。我们这套系统中,采用的是利用Mybatis的plugin机制,在底层SQL解析时替换增加过滤条件。
    这样一套控制机制存在很明显的优缺点,首先缺点:

    1. 适用性有限,基于底层的Mybatis。
    2. 方言有限,针对了某种数据库(我们使用Mysql),而且由于需要在底层解析处理条件所以有可能造成不同的数据库不能兼容。当然Redis和NoSQL也无法限制。

    当然,假如你现在就用Mybatis,而且数据库使用的是Mysql,这方面就没有太大影响了。

    接下来说说优点:

    1. 减少了接口数量及接口复杂度。原本针对不同的角色,可能会区分不同的接口或者在接口实现时利用流程控制逻辑来区分不同的条件。有了数据权限控制,代码中只用写基本逻辑,权限过滤由底层机制自动处理。
    2. 提高了数据权限控制的灵活性。例如原本只有主管能查本部门下组织架构/订单数据,现在新增助理角色,能够查询本部门下组织架构,不能查询订单。这样的话普通的写法就需要调整逻辑控制,使用数据权限控制的话,直接修改配置就好。

    数据权限实现

    上一节就提及了实现原理,是基于Mybatis的plugins(查看官方文档)实现。

    MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
    Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    ParameterHandler (getParameterObject, setParameters)
    ResultSetHandler (handleResultSets, handleOutputParameters)
    StatementHandler (prepare, parameterize, batch, update, query)

    Mybatis的插件机制目前比较出名的实现应该就是PageHelper项目了,在做这个实现的时候也参考了PageHelper项目的实现方式。所以权限控制插件的类命名为PermissionHelper。
    机制是依托于Mybatis的plugins机制,实际SQL处理的时候基于jsqlparser这个包。
    设计中包含两个类,一个是保存角色与权限的实体类命名为PermissionRule,一个是根据实体变更底层SQL语句的主体方法类PermissionHelper。

    首先来看下PermissionRule的结构:

    public class PermissionRule {
    
        private static final Log log = LogFactory.getLog(PermissionRule.class);
        /**
         * codeName<br>
         * 适用角色列表<br>
         * 格式如: ,RoleA,RoleB,
         */
        private String roles;
        /**
         * codeValue<br>
         * 主实体,多表联合
         * 格式如: ,SystemCode,User,
         */
        private String fromEntity;
        /**
         * codeDesc<br>
         * 过滤表达式字段, <br>
         * <code>{uid}</code>会自动替换为当前用户的userId<br>
         * <code>{me}</code> main entity 主实体名称
         * <code>{me.a}</code> main entity alias 主实体别名
         * 格式如:
         * <ul>
         * <li>userId = {uid}</li>
         * <li>(userId = {uid} AND authType > 3)</li>
         * <li>((userId = {uid} AND authType) > 3 OR (dept in (select dept from depts where manager.id = {uid})))</li>
         * </ul>
         */
        private String exps;
    
        /**
         * codeShowName<br>
         * 规则说明
         */
        private String ruleComment;
    
    }
    

    看完这个结构,基本能够理解设计的思路了。数据结构中保存如下几个字段:

    • 角色列表:需要使用此规则的角色,可以多个,使用英文逗号隔开。
    • 实体列表:对应的规则应用的实体(这里指的是表结构中的表名,可能你的实体是驼峰而数据库是蛇形,所以这里要放蛇形那个),可以多个,使用英文逗号隔开。
    • 表达式:表达式就是数据权限控制的核心了。简单的说这里的表达式就是一段SQL语句,其中设置了一些可替换值,底层会用对应运行时的变量替换对应内容,从而达到增加条件的效果。
    • 规则说明:单纯的一个说明字段。

    核心流程
    系统启动时,首先从数据库加载出所有的规则。底层利用插件机制来拦截所有的查询语句,进入查询拦截方法后,首先根据当前用户的权限列表筛选出PermissionRule列表,然后循环列表中的规则,对语句中符合实体列表的表进行条件增加,最终生成处理后的SQL语句,退出拦截器,Mybatis执行处理后SQL并返回结果。

    讲完PermissionRule,再来看看PermissionHelper,首先是头:

    @Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
            @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
    public class PermissionHelper implements Interceptor {
    }
    

    头部只是标准的Mybatis拦截器写法,注解中的Signature决定了你的代码对哪些方法拦截,update实际上针对**修改(Update)、删除(Delete)生效,query是对查询(Select)**生效。

    下面给出针对Select注入查询条件限制的完整代码:

    
        private String processSelectSql(String sql, List<PermissionRule> rules, UserDefaultZimpl principal) {
            try {
                String replaceSql = null;
                Select select = (Select) CCJSqlParserUtil.parse(sql);
                PlainSelect selectBody = (PlainSelect) select.getSelectBody();
                String mainTable = null;
                if (selectBody.getFromItem() instanceof Table) {
                    mainTable = ((Table) selectBody.getFromItem()).getName().replace("`", "");
                } else if (selectBody.getFromItem() instanceof SubSelect) {
                    replaceSql = processSelectSql(((SubSelect) selectBody.getFromItem()).getSelectBody().toString(), rules, principal);
                }
                if (!ValidUtil.isEmpty(replaceSql)) {
                    sql = sql.replace(((SubSelect) selectBody.getFromItem()).getSelectBody().toString(), replaceSql);
                }
                String mainTableAlias = mainTable;
                try {
                    mainTableAlias = selectBody.getFromItem().getAlias().getName();
                } catch (Exception e) {
                    log.debug("当前sql中, " + mainTable + " 没有设置别名");
                }
    
    
                String condExpr = null;
                PermissionRule realRuls = null;
                for (PermissionRule rule :
                        rules) {
                    for (Object roleStr :
                            principal.getRoles()) {
                        if (rule.getRoles().indexOf("," + roleStr + ",") != -1) {
                            if (rule.getFromEntity().indexOf("," + mainTable + ",") != -1) {
                                // 若主表匹配规则主体,则直接使用本规则
                                realRuls = rule;
    
                                condExpr = rule.getExps().replace("{uid}", UserDefaultUtil.getUserId().toString()).replace("{bid}", UserDefaultUtil.getBusinessId().toString()).replace("{me}", mainTable).replace("{me.a}", mainTableAlias);
                                if (selectBody.getWhere() == null) {
                                    selectBody.setWhere(CCJSqlParserUtil.parseCondExpression(condExpr));
                                } else {
                                    AndExpression and = new AndExpression(selectBody.getWhere(), CCJSqlParserUtil.parseCondExpression(condExpr));
                                    selectBody.setWhere(and);
                                }
                            }
    
                            try {
                                String joinTable = null;
                                String joinTableAlias = null;
                                for (Join j :
                                        selectBody.getJoins()) {
                                    if (rule.getFromEntity().indexOf("," + ((Table) j.getRightItem()).getName() + ",") != -1) {
                                        // 当主表不能匹配时,匹配所有join,使用符合条件的第一个表的规则。
                                        realRuls = rule;
                                        joinTable = ((Table) j.getRightItem()).getName();
                                        joinTableAlias = j.getRightItem().getAlias().getName();
    
                                        condExpr = rule.getExps().replace("{uid}", UserDefaultUtil.getUserId().toString()).replace("{bid}", UserDefaultUtil.getBusinessId().toString()).replace("{me}", joinTable).replace("{me.a}", joinTableAlias);
                                        if (j.getOnExpression() == null) {
                                            j.setOnExpression(CCJSqlParserUtil.parseCondExpression(condExpr));
                                        } else {
                                            AndExpression and = new AndExpression(j.getOnExpression(), CCJSqlParserUtil.parseCondExpression(condExpr));
                                            j.setOnExpression(and);
                                        }
                                    }
                                }
                            } catch (Exception e) {
                                log.debug("当前sql没有join的部分!");
                            }
                        }
                    }
                }
                if (realRuls == null) return sql; // 没有合适规则直接退出。
    
                if (sql.indexOf("limit ?,?") != -1 && select.toString().indexOf("LIMIT ? OFFSET ?") != -1) {
                    sql = select.toString().replace("LIMIT ? OFFSET ?", "limit ?,?");
                } else {
                    sql = select.toString();
                }
    
            } catch (JSQLParserException e) {
                log.error("change sql error .", e);
            }
            return sql;
        }
    

    重点思路
    重点其实就在于Sql的解析和条件注入,使用开源项目JSqlParser

    • 解析出MainTable和JoinTable。from之后跟着的称为MainTable,join之后跟着的称为JoinTable。这两个就是我们PermissionRule需要匹配的表名,PermissionRule::fromEntity字段。
    • 解析出MainTable的where和JoinTable的on后面的条件。使用and连接原本的条件和待注入的条件,PermissionRule::exps字段。
    • 使用当前登录的用户信息(放在缓存中),替换条件表达式中的值。
    • 某些情况需要忽略权限,可以考虑使用ThreadLocal(单机)/Redis(集群)来控制。

    结束语

    想要达到无感知的数据权限控制,只有机制控制这么一条路。本文选择的是通过底层拦截Sql语句,并且针对对应表注入条件语句这么一种做法。应该是非常经济的做法,只是基于文本处理,不会给系统带来太大的负担,而且能够达到理想中的效果。大家也可以提出其他的见解和思路。

    展开全文
  • 通用数据权限的设计思路

    万次阅读 2020-01-02 09:25:30
    根据目前的调研情况,有两种数据级别权限设计思路,都可以实现对人员访问的数据权限控制,从而实现不同的人员能够看到不同的数据,例如经理能够看到其部门下所有人的数据,而单个的员工只能看到自己的数据。...

    接着上个襄阳项目的需要, 目前的项目情况是,一期已经把功能权限做完了,可以对不同用户的不同权限功能做到限制,现在需要做数据的权限,不同的用户看到不同的数据。

    根据目前的调研情况,有两种数据级别权限设计思路,都可以实现对人员访问的数据权限控制,从而实现不同的人员能够看到不同的数据,例如经理能够看到其部门下所有人的数据,而单个的员工只能看到自己的数据。用户拥有的权限越大,能看到的数据就越多。

    第一种是使用先在网关进行权限的判断,此用户有这个操作的权限,再进行查询,查询时根据参数进行SQL的拼接。网关鉴定是否能访问以及转发请求,对SQL的拼接和处理在各微服务自己内部进行,服务A/B/C都需要自己写需要查询的参数。如下图所示

    使用此方式存在一些弊端 ,使用SQL拼接的方式不利于统一管理权限,每个项目有自己的过滤方式,拼接SQL的方式,不利于统一管理,也不方便后期可能出现的规则修改,因为已经硬编码到代码中,需要各个服务自己修改一遍。

    优点很明显,足够很简单,只需要根据相应的规则调整SQL就可以了。

     

    第二种方式,专门抽离一个权限鉴定的服务,所有到达网关的请求后,网关去调用权限鉴定服务,进行访问的URL的权限判断,如果鉴定成功,则能够访问,网关转到请求到对应的服务,否则网关直接返回,提示用户没有权限访问。数据权限的控制依然需要各个微服务硬编码到各自的项目代码中去。如下图所示

     

    目前没有什么方式能做这种通用的数据级权限控制(因为涉及到各个服务的查询SQL进行过滤,拼接,在这个权限鉴定服务中没法实现这样的功能,对各种不同的查询的SQL进行拼接,过滤)。

     

    这种方式相对于第一种把对接口的访问权限校验抽离出来了,减轻了网关的压力,依然是各个服务自己实现数据查询的控制。

     

    现在的思路是: 

    新建一个mybatis的插件, 让各个服务都依赖这个(java项目可以继承, 其他的php得服务暂时不考虑), 这个插件的作用就是对

    SQL进行过滤, 拼接. 鉴权服务会对配置的数据的权限进行读取, 形成一个SQL, 以传递参数的形式, 返回给网关, 网关转发给其他的微服务, 服务获取到携带的SQL参数, 利用插件对SQL进行拼接, 过滤, 就可以查询出来数据了.进行数据的返回.

     

    下面是目前的一部分代码, 希望能够给各位一些启示:

    package co.filter;
    
    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.executor.Executor;
    import org.apache.ibatis.executor.statement.StatementHandler;
    import org.apache.ibatis.mapping.BoundSql;
    import org.apache.ibatis.mapping.MappedStatement;
    import org.apache.ibatis.plugin.*;
    import org.apache.ibatis.session.ResultHandler;
    import org.apache.ibatis.session.RowBounds;
    import org.springframework.stereotype.Component;
    
    import java.util.Properties;
    
    /**
     * @author zhangke
     * @time 2019年12月23日10:10:35
     */
    @Component
    @Slf4j
    @Intercepts({//注意看这个大花括号,也就这说这里可以定义多个@Signature对多个地方拦截,都用这个拦截器
            @Signature(type = Executor.class,//这是指拦截哪个接口
                    method = "query",//这个接口内的哪个方法名,不要拼错了
                    //这是拦截的方法的入参,按顺序写到这,不要多也不要少,如果方法重载,可是要通过方法名和入参来确定唯一的
                    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
    })
    public class AuthorityFilterPlugin implements Interceptor {
    
        /**
         * 这里是每次执行操作的时候,都会进行这个拦截器的方法内
         *
         * @param invocation
         * @return
         * @throws Throwable
         */
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            //对sql进行处理
            Object[] queryArgs = invocation.getArgs();
            MappedStatement mappedStatement = (MappedStatement) queryArgs[0];
            Object parameter = queryArgs[1];
            BoundSql boundSql = mappedStatement.getBoundSql(parameter);
            // 获取到SQL ,进行调整
            String sql = boundSql.getSql();
            if (!sql.contains("limit")) {
                sql = sql + "limit 1";
            }
            log.info("正在执行的sql是: {}", sql);
            return invocation.proceed();
        }
    
        /**
         * 主要是为了把这个拦截器生成一个代理放到拦截器链中
         */
        @Override
        public Object plugin(Object target) {
            //官方推荐写法
            return Plugin.wrap(target, this);
        }
    
    
        /**
         * 插件初始化的时候调用,也只调用一次,插件配置的属性从这里设置进来
         *
         * @param properties
         */
        @Override
        public void setProperties(Properties properties) {
        }
    
    
    }

     

    下面是过滤的参数的设置.

    package com.curefun.authority.filter;
    
    
    import lombok.Data;
    import java.util.List;
    
    /**
     * 过滤器的参数实体类,用于拼接SQL用的
     *
     * question:
     * 1.分页怎么办? mybatis的分页插件之前执行...
     * 2.夺标关联查询怎么办? 别名的设置?
     * 3.这是单独的一个服务,获取到的sql, 可以用拼接url的形式转发给其他服务,其他服务又需要改造,添加mybatis 的插件.
    4.一个查询方法对应的多个sql的时候,拼接哪一个?怎么指定?
     *
     */
    @Data
    public class FilterVO {
    
    
        private String id;
        /**
         * 服务名
         */
        private String serviceName;
        /**
         * 请求的url
         */
        private String requestUrl;
        /**
         * 类型 0:不启用   1:启用
         */
        private Integer filterState;
        /**
         * 参数的具体值
         */
        private List<ColumnDataVO> params;
    
    
    }
    
    
    package com.curefun.authority.filter;
    
    import lombok.Data;
    
    
    /**
     * 一个查询的所有要拼接的参数
     */
    @Data
    public class ColumnDataVO<T> {
    
        /**
         * 列名字
         */
        private String columnName;
        /**
         * 列值,String,Number,List,Set,类型,或者Pair的类型
         */
        private T value;
        /**
         * 类型
         */
        private OperationEnum operationEnum;
        /**
         * 参数类型
         */
        private ParamTypeEnum paramEnum;
        /**
         * 排序,从0 开始,数字越小拼接就越靠前,不能为空
         */
        private int orderNum;
    
    
        public ColumnDataVO(String columnName, T value, OperationEnum operationEnum, ParamTypeEnum paramEnum, int orderNum) {
            this.columnName = columnName;
            this.value = value;
            this.operationEnum = operationEnum;
            this.paramEnum = paramEnum;
            this.orderNum = orderNum;
        }
    
        public ColumnDataVO() {
        }
    
    }
    
    
    package com.curefun.authority.filter;
    
    /**
     * 操作类型化的美剧
     */
    public enum OperationEnum {
        EQUAL(1,"=","相等"),
        GT(2,">","大于"),
        LT(3,"<","小于"),
        GTE(4,">=","大于等于"),
        LTE(5,"<=","小于等于"),
        IN(6,"in","包含"),
        BETWEEN(7,"between","介于之间"),
        ;
    
        private int typeCode;
        private String operationSymbol;
        private String message;
    
        OperationEnum(int typeCode, String operationSymbol, String message) {
            this.typeCode = typeCode;
            this.operationSymbol = operationSymbol;
            this.message = message;
        }
    
    
        public int getTypeCode() {
            return typeCode;
        }
    
        public String getOperationSymbol() {
            return operationSymbol;
        }
    
        public String getMessage() {
            return message;
        }
    }
    
    
    
    package com.curefun.authority.filter;
    
    /**
     * 参数类型
     */
    public enum ParamTypeEnum {
    
    
        STRING(1,"字符串"),
        NUMBER(2,"数字"),
        LIST(3,"List"),
        SET(4,"Set"),
        /**
         * HH:mm:ss
         */
        TIME(5,"时间"),
        /**
         * yyyy-MM-dd HH:mm:ss
         */
        DATEtIME(6,"时间"),
        /**
         * 双值,用于between的前后范围
         */
        PAIR(7,"双值"),
        ;
    
        private int type;
        private String message;
    
        ParamTypeEnum(int type, String message) {
            this.type = type;
            this.message = message;
        }
    
        public int getType() {
            return type;
        }
    
        public String getMessage() {
            return message;
        }
    }
    
    

     

    未完成的部分:

    1.根据FilterVO 做SQL的拼接.

    2.以上的四个问题,有待于解决,

    3.mybatis的过滤器中,怎么根据sql进行正确的拼接, 而且需要考虑效率问题.

    5.系统配置的表设计,我这里没做,我想过一下, 但是没有把表设计出来,其实是个一对多的表设计, 两张表就够了.

     

    好了, 就写到这里, 之所以不往下写了, 是因为业务暂时没有这方面的需求, 我这个只是属于前期的探索, 这个已经完成的项目, 还有很大的变数, 还未交付. 所以不做了. 好好复习.

    这里放一个别人设计的一张表结构: 

    CREATE TABLE `sys_acl_data` (
      `id` int(11) NOT NULL,
      `acl_id` int(11) NOT NULL COMMENT '对应权限表主键',
      `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态,1:可用,0:不可用',
      `param` varchar(20) NOT NULL DEFAULT '' COMMENT '参数',
      `operation` int(11) NOT NULL DEFAULT '0' COMMENT '操作类型,0;等于,1:大于,2:小于,3:大于等于,4:小于等于,5:包含,6:介于之间,。。。',
      `value1` varchar(100) NOT NULL DEFAULT '0',
      `value2` varchar(100) NOT NULL DEFAULT '0',
      `next_param_op` int(11) NOT NULL DEFAULT '0' COMMENT '后续有参数时连接的关系,0:没有其他参数控制,1:与&&,2:或||',
      `seq` tinyint(4) NOT NULL DEFAULT '0' COMMENT '顺序',
      PRIMARY KEY (`id`),
      KEY `idx_acl_id` (`acl_id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='数据权限表';

    有可取之处, 不过要想在实际生产环境中适用, 还有很多的工作需要做, 

     

    如果您觉得写得不多, 可以请作者喝一杯咖啡

     

     

     

    展开全文
  • 从零开始java数据权限篇:数据权限

    千次阅读 2019-09-09 11:59:05
    一:数据权限的产生 二:数据权限的数据切割 1.数据对应的层级图 2.用户数据查询 3.用户流程管理 4.部门-岗位-公司查询拓扑图 三.说明 一:数据权限的产生 在一个后管系统中,由2个最重要的权限划分。第一...

    目录

    一:数据权限的产生

    二:数据权限的数据切割

    1.数据对应的层级图

    2.用户数据查询

    3.用户流程管理

    4.部门-岗位-公司查询拓扑图

    三.说明



    一:数据权限的产生

      在一个后管系统中,由2个最重要的权限划分。第一个访问权限,通过控制访问路径、请求来控制访问的权限,第二个是数据权限,通过一系列的分割策略来对用户进行管理。

     访问权限,可以这么说访问权限可以通过集成框架(比如shiro或者Spring seurity或者oauth2)来控制。所以严格意义上来说访问权限是一个技术框架

     数据权限,数据权限用来对用户信息进行管理包括但是不限于用户列表查询、子母公司数据划分以及工作流等的一系列。但是一个比较尴尬的问题就是,数据权限实则是一个业务层,它是由具体的公司业务来决定的。当然,凡是可以抽象出来的我们都可以抽象出来。目前主流的的数据权限控制以 部门-岗位-上级-本级 这样一个递减结构来做通用式的管理。

      也翻看了一些所谓的数据级权限中间件比如目前介绍较多的Ralasafe,实则上就是在访问Sql上做一些侵入式Sql限制。基于此,我们完全可以在项目中使用AOP做。但是很尴尬的是目前这个数据级的中间件已经停止维护。

     

    二:数据权限的数据切割

      在上面,我们谈到一般都是以 部门-岗位-上级-本级 的结构来做数据权限控制。但是一般项目中会把上级-本级合并产生一个简单的结构:部门-岗位-用户 来维护数据权限。

    1.数据对应的层级图

                                                   

      首先:部门对于用户而言,是用户固有的一个属性,只能是一对一关系。

                  岗位对于用户而言 ,是由上级或者管理员分配的一个属性,可以是一对多(即一个用户被指向多个部门的负责人,但是

                                                        他本身只有某一个部门的属性)。

                  部门对岗位而言,同时也是一对多的关系,即一个部门有多个角色。

    2.用户数据查询

                   

     然后,我们针对组织结构细分:

     (1)公司结构以及部门结构:管理层级(具有本部门的最高权限)以及一般员工(只有自我查询权限)

                                                           其中总公司的管理层具有整个结构的最高权限。

    (2)单个单位中的结构:单个单位内的负责人具有最高单位权限

                                                  单个单位的次级负责人具有管理名下的权限。(即上级-下级的管理权限)

                                                   单个单位内的普通员工只具有自身的权限

    3.用户流程管理

      由于后期要整合工作流,因此这里我们将流程梳理清晰。

     

    4.部门-岗位-公司查询拓扑图

    目前,主要角色分为以下几个:

    (1)普通员工(仅能看到自己的信息)

    (2)部门管理人(仅能看到本部门)

    (3)分公司董事(除了董事会部门的其他分公司所有的部门)

    (4)分公司总负责人(看到分公司所有的信息)

    (5)总公司董事会(除了总公司董事会之外的所有总,自公司的所有部门)

    (6)总董事

    部门-岗位-董事表拓扑(比较简单,就不做UML了)

    据图查询:

    (1)普通员工:这个就不用说了

    (2)部门负责人:查询 用户表中 部门id一样的即可

    (3)分公司董事会:第一步,根据用户表中的部门id去查询部门表

                                          第二步:根据部门表中的所有上级id,取出公司统一的开头(一般第一位为总公司id,第二位为

                                                          分公司id,第三位包括以后是部门层级一级一级玩下的id,以逗号隔开)

                                         第三步:根据公司统一的开头曲模糊查询所有的部门id(这里在程序中控制得当,是完全可以的)

                                         第四步:根据上一步的部门id,同时去掉本身的部门id(即董事会部门的id),然后反查用户表

     (4)分公司负责人 :在上一个的第四步,不去掉本身的部门id

     (5)总公司董事会:查询所有的,除了自身的部门id

    (6)总公司负责人:全查

    (7)自定义:选择部门,然后插入  岗位-部门表做联查(对,这个表只在这里用到)

    三.说明

      上面所描述的是通常情况下,一般都是根据具体业务做管理

     

     

    展开全文
  • 文章目录前言一、什么是数据权限二、新建如下表三、注意 前言 这一章的部分是我写到现在最累的一部分,累就累在逻辑的处理上,也让我发现了数据库设计之初的一些命名问题(之后再解决这个问题)。 就像是用户表中的id...
  • 基于SpringAOP实现数据权限控制

    万次阅读 2018-01-05 19:24:14
    基于SpringAOP实现数据权限控制 在此主要是实现对用户查询数据返回字段的控制。比如一个表格有A,B,C,D,E五列,用户U1只能查看A,B,C三列。 此文章讲述的内容并不能实现在查询时仅查询A,B,C三列,而是在查询...
  • 基于mybatis拦截器实现数据权限

    万次阅读 2018-12-02 11:41:38
    数据权限是很多系统常见的功能,实现的方式也是很多的,最近在做项目的时候,自己基于mybatis拦截器做了一个数据权限的功能。 **功能设计 a) 需要做数据权限功能的表加上一个权限id字段。 权限id可以不仅仅是...
  • 文章目录权限分类数据权限实现MyBatis的拦截器简介具体实现数据库设计权限类型设计拦截器实现参数传递加载拦截器忽略拦截使用示例 今天来整理分享下常用的权限设计,以及基于Mybatis-plus实现的一套数据权限的实现。...
  • 若依数据权限

    千次阅读 2020-12-06 00:33:45
    若依数据权限是通过使用自定义注解,AOP拦截,实现将数据权限的sql插入Mapper文件中,从而达到控制数据权限的目的。
  • 浅谈权限(功能权限&数据权限

    万次阅读 2020-02-28 11:38:18
    一般企业上的权限部分,都是区分为功能权限和数据权限。 功能权限: 功能权限,就是用户登录后,能看到哪些菜单,能看到哪些按钮,能执行哪些操作的权限。 一般,功能权限,已经都有很成熟的业内方案和框架了。 比如...
  • 基于若依框架的数据权限

    千次阅读 热门讨论 2020-11-21 20:15:24
    数据权限 数据权限和菜单权限不一样 菜单权限: 根据不同的性质的用户,显示不同的菜单。 数据权限: 设置权限,不同的用户只能访问本用户的数据,或者本部门的数据。当然对于特殊的领导可以跨部门访问数据。这些...
  • 功能权限和数据权限管理的实现

    万次阅读 2018-01-10 11:36:19
    功能权限和数据权限管理的实现 转自 http://blog.csdn.net/xuanbg/article/details/23286717 标签:企业 /权限 1 引言 权限,可分为“功能(操作)权限”和数据权限两种,在系统中,两种...
  • 通用权限管理设计 之 数据权限

    万次阅读 2018-01-04 10:09:40
    本文将对这种设计思想作进一步的扩展,介绍数据权限的设计方案。 权限控制可以理解,分为这几种 : 【功能权限】:能做什么的问题,如增加产品。 【数据权限】:能看到哪些数据的问题,如查看本人的所有订单。 ...
  • 好处就是如果用户全体特别大,分配权限就能累死人,基于RBAC更适合企业应用得权限控制 2. RBAC得四大模型 RBAC根据这套模型功能的复杂程度不同,由简单到复杂又可以分为RBAC-0、RBAC-1、RBAC-2、RBAC-3四个层级。 ...
  • 菜单访问权限控制 配置helloword菜单【系统管理】-【菜单管理】 其中前端组件配置相对src/views/目录下的 目录名+文件名 例如页面src/views/jeecg/helloworld.vue 前端组件配置 jeecg/helloworld 用户角色授权...
  • 数据权限系统

    万次阅读 2018-04-08 12:44:45
    在之前写过一篇关于菜单权限系统的设计,所以为了完善整个权限系统的模型,决定把数据权限也做一个总结。菜单权限管理系统目标实现对数据的权限控制。简单的来说,就是决定谁可以操作(增删改查)哪些数据。该权限...
  • 数据权限控制

    千次阅读 2018-08-28 10:18:01
    查询参数经过控制层,aop组件对参数进行拦截,读取redis缓存中的数据权限数据,进而对参数进行重新组装。 从新组装的查询参数传递到业务处理层。 业务处理层将查询参数传递到数据访问层。 Sql分析组装组件对查询...
  • 《JeeSite4.x数据权限教程》全课程时长:04:56:49 https://space.bilibili.com/383413957/channel/detail?cid=75515 并额外免费赠送45套全栈工程师视频教程资源地址 赠送教程列表: 免费 《JeeSite4.x数据...
  • 权限管理 一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源,不多不少。权限管理几乎出现在任何系统里面,只要有用户和密码的系统。 分类 DataPermissionInterceptor ...
  • 数据权限设计(转载)

    万次阅读 多人点赞 2019-01-01 13:57:49
    几乎在任何一个系统中,都离不开权限的设计,权限设计 = 功能权限 + 数据权限,而功能权限,在业界常常是基于RBAC(Role-Based Access Control)的一套方案。而数据权限,则根据不同的业务场景,则权限却不尽相同,...
  • 数据权限管理中心由于公司大部分项目都是使用mybatis,也是使用mybatis的拦截器进行分页处理,所以技术上也直接选择从拦截器入手需求场景第一种场景:行级数据处理原sql:select id,username,region from sys_user ;...
  • ruoyi框架数据权限

    千次阅读 2020-07-14 09:54:54
    ruoyi框架数据权限 1.页面设置对应角色的数据权限 2.service层加上@DataScope注解 部门表的别名deptAlias = “d” 用户表的别名userAlias = “u” 3.sql上加上过滤
  • 针对不同用户,在数据查询时要在SQL上拼上可以访问的部门机构部分。...这里看到一种比较好的方法可以实现数据权限。 使用方法 其中tableAlias为SQL中表的别名。 @DataAuth(tableAlias = "s") public Result...
  • 大数据平台数据权限管理设计

    千次阅读 2020-02-25 15:22:43
    对于数据开发者,主要通过分配IAM控制AWS的操作权限;对于team的所有人都是通过分配aws的ak,sk在本地进行操作赋权;随着数据平台的不断的丰富和完善,需要在各组件之上做认证,鉴权和审计等管理,数...
  • 权限设计(资源权限和数据权限

    千次阅读 2018-11-12 11:25:00
    场景:对一个角色实例授权数据权限,这里所说的角色实例是比如有两个用户张三和李四,具有相同的角色管理员,那对应的角色实例分别为 管理员张三,管理员李四,这里想要对管理员李四进行一个特殊授权,可以查看指定...
  • 权限管理-数据权限

    千次阅读 2016-05-12 11:36:56
    权限管理-数据权限
  • 实现数据权限控制的一种方法

    万次阅读 2018-03-19 00:00:01
    可以设置用户可以查看哪些部门的数据 这种权限的控制,一般称为数据权限,与之对应的功能权限,则是系统中哪些功能可以使用——①菜单、按钮等元素能正常显示;②如果用户访问了本身不可见的功能,系统也能阻止...
  • 若依数据权限

    千次阅读 2019-05-31 19:59:31
    "查询未添加泛型, 数据权限未控制" ) ; } } } } /** * 是否存在注解,如果存在就获取 */ private DataScope getAnnotationLog ( JoinPoint joinPoint ) { Signature signature = ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,315,205
精华内容 526,082
关键字:

数据权限