1.首先说说功能特性
数据行权限--让不同的人看到不同的数据行的权限控制数据列权限--对使用者限制其访问其些列或某些列中的数据的权限控制2.数据权限说起来比较简单,但是实际实现过程中,也是非常复杂的,比如,同样是select表,可能是对原样的表名及字段名进行访问,也可以是把表名和字段名取了别名,另外还会涉及到复杂的查询和子查询。
3.同样的数据列权限,还有的用场景是不同的权限的人员可以看到的权限范围是不一样的,比如:HR系统中,不同管理级别的HR可以看到的员工的工资范围是不同的,比如:只能看到被授权访问级别人员的工资信息,但是其它信息都是可以访问的。传统的做法下,这种情况只有写程序一种方案了,但是如果这个时候有许多种组合,这个代码写出来就没有办法看了。
4.在上篇文章中,本人有提到:一个好的数据权限方案要考虑以下方面:
对程序员透明:整个方案对于程序员们来说,不论是前台还是后台的程序员们来说,越透明越好,他们最好不知道有这么回事情与不同的用户权限能较好集成:作为一个数据权限系统来说,肯定要和用户、角色、组织机构什么的数据打交道。如果这个方案只能和某一种用户权限系统集成,那么它充其量只能算是一个可用的解决方案,但是注定算不是上好的解决方案。性能损失最少:我们说,要增加新的功能或限制的过程中,肯定会对性能方面有一定影响的,怎么样能把性能损失降低,甚至能提升一定的性能是设计师们永恒的主题。对于分页能良好支持:上面有说到当采用了行权限时,有些行是不能显示给用户的,这个时候不能对分页有影响,比如本来是一页是10条的,现在忽然变成8条,甚至在极端情况下变成0条,这样的用户体验就非常差了。一次设定到处生效,哪怕是不同的ORMapping方案也都起作用:我们知道,现在采用唯一的一种ORMapping解决方案的场景已经越来越少了,不同的方案都有自己的优缺点,它可能只适应某种前置条件下的应用场景,实际应用的情况下根据场景不同使用不同的解决方案也是非常普遍的,这就要求解决方案有一定的普适定。跨数据库:一个项目,它的运行环境基本上是确定的。但是对于一个产品来说,根据用户的实际场景不同,可能对于数据库也有不同的要求,这个时候就需要对不同的数据库有支持,不能对于A数据库是支持的,对于B数据库的场景下,就出异常或者结果不正确了。5.在此次实现过程中,我们对数据权限进行了进一步的引申,不仅针对select提供了数据权限,还针对insert、delete、update语句提供了权限控制,也就是说,通过tinydac,甚至可以限制你插入或修改数据的值的范围,比如:你的权限插入时只能插入100以内的数据,当代试图插入大于100的数值的时候,就会出现SQL执行异常,提醒你执行范围超出了。
6.同样,还有多少种不用的用法,需要看你的脑洞可以开多大了。
7.当然,本次的实现方案,就需要比较优雅解决这些难题。我的任务在于提出问题和给出解决方案,编码这种事情当然就直接让小弟们去完成就好了,偶的工作在于喝茶和“骂”人——直到实现的代码和偶的期望比较接近。
8.下面我们来看看几个示例:
9.数据规则配置
data-access-controlsdata-access-controlid"insertDataAccessControl"connectiondriver"org.h2.Driver"user-name"sa"password"123456"url"jdbc:h2:./dbfile/dac_simple;modeMYSQL;DB_CLOSE_ON_EXITFALSE"class"org.tinygroup.dac.config.DataSourceMode"/connectionaccess-control-ruletable-rulestable-ruletable-name"custom"insert-rulefilter-rulesfilter-rule!--过滤访客插入custom_group,age--expression![CDATA[(#is_guest#!null#is_guest#1)]]/expressionfilter-columnsfilter-columncolumn-name"custom_group"/filter-columnfilter-columncolumn-name"age"/filter-column/filter-columns/filter-rule/filter-rulesvalue-check-rulesvalue-check-rulecolumn-name"name"expression![CDATA[#name#.contains("sds")]]/expression/value-check-rule/value-check-rules/insert-rule/table-rule!--非自增长测试字段过滤为全空的情况--table-ruletable-name"score"insert-rulefilter-rulesfilter-rule!--过滤访客插入custom_group,age--expression![CDATA[(#score#!null(#score#100||#score#0))]]/expressionfilter-columnsfilter-columncolumn-name"score"/filter-column/filter-columns/filter-rulefilter-rule!--过滤敏感字段:gcd--expression![CDATA[(#name#!null(#name#.contains("gcd")))]]/expressionfilter-columnsfilter-columncolumn-name"name"/filter-column/filter-columns/filter-rule/filter-rules/insert-rule/table-rule/table-rules/access-control-rule/data-access-control/data-access-controls10.大概解释一下上面的意思:
11.首先配置了数据权限要控制的真实物理数据源,目前支持两种形式的方式,一种是URL+用户名密码的方式,另外一种是JNDI数据源的形式。
12.接下来配置了数据访问的规则,可以分别配置SELECT,INSERT,UPDATE,DELETE的规则,以便对4种SQL语句进行精确控制。
insert-rulefilter-rulesfilter-rule!--过滤访客插入custom_group,age--expression![CDATA[(#is_guest#!null#is_guest#1)]]/expressionfilter-columnsfilter-columncolumn-name"custom_group"/filter-columnfilter-columncolumn-name"age"/filter-column/filter-columns/filter-rule/filter-rulesvalue-check-rulesvalue-check-rulecolumn-name"name"expression![CDATA[#name#.contains("sds")]]/expression/value-check-rule/value-check-rules/insert-rule13.上面的这段配置表示,对于访客来说,插入数据的时候,custom_group字段和age字段是不可以被插入的,即使程序在写的时候有指定值,那么这两个字段也会被忽略。
14.另外,也对插入的值进行了安全检查,上面的例子中定义了要插入的名字字段中必须包含"sds"字符串。
publicvoidtestValueCheck()throwsSQLException{try{//'asda'不符合条件stmt.execute("insertintocustom(name,age)values('asda',11)");}catch(DataAccessControlRuntimeExceptione){assertEquals("0TE120120001",e.getErrorCode().toString());}//多行插入测试try{stmt.execute("insertintocustom(name,age,custom_group)values('sdsaaa',19,''),('sdsbbb',20,'normal')");}catch(DataAccessControlRuntimeExceptione){assertEquals("0TE120120001",e.getErrorCode().toString());}}15.上面是做测试用例,验证在插入一条或多条数据的时候,能不能对数据进行相关的合法性检查。
16.通过在数据访问权限中控制,会有一个非常好的地方,就是不管程序员用怎么编写,用什么框架编写,只要不符合规则,统统都会被拦住,绝对不会有一个漏网之鱼发生。
17.上面只是小试牛刀,接下来我们看看其它的语句的处理:
access-control-ruletable-rulestable-ruletable-name"custom"delete-rulevalue-check-rulesvalue-check-rulecolumn-name"name"expression![CDATA[#name#.contains("sds")]]/expression/value-check-rule/value-check-rules/delete-rule/table-rule/table-rules/access-control-rule18.上面定义了一条规则,意思是只有name字段的值是以sds开头的才可以删除。
preparedStatementconn.prepareStatement("deletefromcustomwherename?");preparedStatement.setString(1,"aaaa");booleanisExceptionfalse;try{preparedStatement.execute();}catch(DataAccessControlRuntimeExceptione){}preparedStatementconn.prepareStatement("deletefromcustomwherename?");preparedStatement.setString(1,"sd");try{preparedStatement.executeUpdate();fail();}catch(DataAccessControlRuntimeExceptione){}preparedStatementconn.prepareStatement("deletefromcustomwherename?");preparedStatement.setString(1,"sds");try{preparedStatement.executeUpdate();}catch(DataAccessControlRuntimeExceptione){fail()}19.从上面可以看到,只有满足条件的时候,才可以正确的删除成功,否则就会抛出数据访问异常。
20.当然,我们在处理的时候,有行权限和列权限。对于行权限,实际上是通过附加条件的方式进行数据过滤(SELECT)或者直接抛出异常的方式(INSERT、DELETE、UPDATE)。对于列权限则有两种处理方式,一种是当SELECT的时候,有些列没有权限时,就直接显示NULL,另外一种情况是,当没有权限访问的时候,把该列置为用记期望的某个值。
filter-rulesfilter-ruleexpression![CDATA[#age#20]]/expressionfilter-columnsfilter-columncolumn-name"age"value"20"/filter-column/filter-columns/filter-rulefilter-ruleexpression![CDATA[#age#20]]/expressionfilter-columnsfilter-columncolumn-name"age"value"1000"/filter-column/filter-columns/filter-rule/filter-rules21.上面的含义是当age20的时候,就把age列返回20;如果年龄大于20,则age列返回1000。这里只是一个示例了,不要太纠结于为什么这里是设置为20而不是30,实际上你可以设置成任意的值,比如-1,用来表示你是没有仅限查看,这样在界面上显示的时候,就可以做相应的处理了:#if(user.age-1)无权查看#end
22.如果不指定value属性,则会被返回为NULL。
publicvoidtestWithSchema()throwsSQLException{//准备测试数据stmt.execute("insertintocustom(id,age,name)values(1,11,'aaa')");stmt.execute("insertintocustom(id,age,name)values(2,50,'bbb')");//测试ContextcontextnewContextImpl();//上下文中放入年龄,不为空的情况下配置condition为【"age"+#s_age#】context.put("s_age",30);//where子句中增加【cus.age30】dataAccessControlContext.setThreadContext(context);//最终sqlSELECTcustom.id,custom.name,custom.ageFROMcustomWHEREage30//H2数据库默认schema是PUBLICResultSetrsstmt.executeQuery("selectPUBLIC.custom.id,PUBLIC.custom.name,custom.ageFROMPUBLIC.custom");resultAssert(rs,newObject[][]{{"bbb",1000}});}23.如上,我们在数据库中插入的是11和50,但是在查询时,本来是50的值的时候,结果返回的反而是1000。
24.上面的示例中,可以看到我们是向contex中放了一个值,实际上你可以放置任意的KV,实际应用中,应该放置的是和用户相关的一些数据对象,这样就可以编写和用户相关的表达式了。
25.当然,实际上应用过程,应用场景会非常复杂,比如有:
26.selectnameasnfromuserasuwhereu.n'123'之类的写法,当然对于TinyDAC来说,还是可以来者通吃,全部正确的支持掉。
27.总之一句话,Tiny的原则就是牺牲自己,愉悦程序员。只有你想不到的,少有我们不支持的。
28.Between,支持:
publicvoidtestBetween()throwsSQLException{stmt.execute("insertintocustom(id,age,name)values(1,19,'aaa')");stmt.execute("insertintocustom(id,age,name)values(2,31,'bbb')");stmt.execute("insertintocustom(id,age,name)values(3,40,'ccc')");stmt.execute("insertintocustom(id,age,name)values(4,50,'ddd')");ContextcontextnewContextImpl();dataAccessControlContext.clearAllValues();//上下文中放入年龄,不为空的情况下配置condition为【"age"+#s_age#】context.put("s_age",30);//where子句中增加【cus.age30】dataAccessControlContext.setThreadContext(context);//真正sqlSELECTid,nameFROMcustomWHEREageBETWEEN31AND40andage30ResultSetrsstmt.executeQuery("selectid,namefromcustomwhereagebetween31and40");resultAssert(rs,newObject[][]{{"bbb"},{"ccc"}});}29.distinct支持:
publicvoidtestDistinct()throwsSQLException{//准备测试数据stmt.execute("insertintocustom(id,age,name)values(1,28,'aaa')");stmt.execute("insertintocustom(id,age,name)values(2,32,'bbb')");stmt.execute("insertintocustom(id,age,name)values(3,31,'ccc')");stmt.execute("insertintocustom(id,age,name)values(4,31,'ddd')");ContextcontextnewContextImpl();//上下文中放入年龄,不为空的情况下配置condition为【"age"+#s_age#】context.put("s_age",30);//where子句中增加【cus.age30】dataAccessControlContext.setThreadContext(context);//最终sqlSELECTDISTINCTageFROMcustomWHEREage30]ResultSetrsstmt.executeQuery("selectdistinctagefromcustom");Object[][]expectedResultnewObject[][]{{1000},{1000}};inti0;while(rs.next()){assertEquals(rs.getObject(1),expectedResult[i++][0]);}assertEquals(i,expectedResult.length);//确保有数据}30.函数依旧支持:
/***concat函数*格式selectCONCAT(aaa,'sds')fromddltest;*/publicvoidtestConCat()throwsSQLException{//准备测试数据stmt.execute("insertintocustom(id,age,name)values(1,28,'aaa')");stmt.execute("insertintocustom(id,age,name)values(2,32,'bbb')");stmt.execute("insertintocustom(id,age,name)values(3,31,'ccc')");stmt.execute("insertintocustom(id,age,name)values(4,31,'ddd')");ContextcontextnewContextImpl();dataAccessControlContext.clearAllValues();//上下文中放入年龄,不为空的情况下配置condition为【"age"+#s_age#】context.put("s_age",30);//where子句中增加【age30】dataAccessControlContext.setThreadContext(context);ResultSetrsstmt.executeQuery("selectid,concat(name,'eee'),agefromcustom");resultAssert(rs,newObject[][]{{"bbbeee",1000},{"ccceee",1000},{"dddeee",1000}});}31.分页:
/***格式SELECT*FROMmytableWHEREmytable.col9LIMIT3,?*格式SELECT*FROMmytableWHEREmytable.col9LIMIT?OFFSET3*/publicvoidtestLimit()throwsSQLException{//准备测试数据stmt.execute("insertintocustom(id,age,name)values(1,28,'aaa')");stmt.execute("insertintocustom(id,age,name)values(2,29,'bbb')");stmt.execute("insertintocustom(id,age,name)values(3,32,'ccc')");stmt.execute("insertintocustom(id,age,name)values(4,31,'ddd')");stmt.execute("insertintocustom(id,age,name)values(5,32,'eee')");stmt.execute("insertintocustom(id,age,name)values(6,33,'fff')");stmt.execute("insertintocustom(id,age,name)values(7,34,'ggg')");stmt.execute("insertintocustom(id,age,name)values(8,35,'hhh')");stmt.execute("insertintocustom(id,age,name)values(9,36,'iii')");stmt.execute("insertintocustom(id,age,name)values(10,37,'jjj')");stmt.execute("insertintocustom(id,age,name)values(11,38,'kkk')");stmt.execute("insertintocustom(id,age,name)values(12,39,'lll')");//实际sql【SELECTage,cus.nameFROMcustom_viewcusWHEREid1LIMIT5OFFSET0】ResultSetrsstmt.executeQuery("selectage,cus.namefromcustom_viewcuswhereid1limit0,5");resultAssert(rs,newObject[][]{{"bbb",1000},{"ccc",1000},{"ddd",1000},{"eee",1000},{"fff",1000}});rsstmt.executeQuery("selectage,cus.namefromcustom_viewcuswhereid1orderbyiddesclimit8,4");resultAssert(rs,newObject[][]{{"ddd",1000},{"ccc",1000},{"bbb",1000}});ContextcontextnewContextImpl();//上下文中放入年龄,不为空的情况下配置condition为【"age"+#s_age#】context.put("s_age",30);//where子句中增加【cus.age30】dataAccessControlContext.setThreadContext(context);//实际sqlSELECTage,cus.nameFROMcustom_viewcusWHEREid1andcus.age30ORDERBYidDESCLIMIT4OFFSET8rsstmt.executeQuery("selectage,cus.namefromcustom_viewcuswhereid1orderbyiddesclimit8,4");resultAssert(rs,newObject[][]{{"ddd",1000},{"ccc",1000}});}32.至此,我们就对数据访问权限有了一个完整的实现,由于测试用例实在太多,有100多个,没有办法一一展示,但是通过上面的一点示例大概可以看出一点意思了。
33.确实,这个问题是一个非常难于解决的问题,我们通过艰苦努力,终于有了一个解,还是一个不错的解。
34.好的架构师就是把不可能变成可能,把可能变成简单。
35.QA:
36.1.这个框架有侵入性么?
37.没有任何侵入性,可以在任何的JAVA环境中使用。
38.2.对JDK版本有要求么?
39.JDK1.5、6、7、8都可以。
40.3.原有的集成此框架有哪些改造的?
41.把原来写的行、列权限的硬变码变成配置,做个切面把用户相关的对象,放到ThreadLocal当中去。
42.4.我们原来用的是Ibatis,Hibernate,SpringJDBCTempalte,etc,可以和这个框架集成么?
43.没有任何问题,只要是低层基于JDBC的数据库访问方式全支持。
44.5.支持非关系型数据库么?
45.理论上也可以支持,但是目前我们只支持关系型数据库。
46.6.这个框架会开源么?
47.嗯,这是一个选项,但是目前还没有开源。
数据权限方案-java数据权限实现方案-java小程序实例大全
浏览量:2042
时间:
来源:wxy49212
版权声明
即速应用倡导尊重与保护知识产权。如发现本站文章存在版权问题,烦请提供版权疑问、身份证明、版权证明、联系方式等发邮件至197452366@qq.com ,我们将及时处理。本站文章仅作分享交流用途,作者观点不等同于即速应用观点。用户与作者的任何交易与本站无关,请知悉。
最新资讯
-

即速应用,赋能企业玩转微信小程序智慧经营
作为国内领军的智慧商业经营服务商,即速应用始终秉承“让每个企业都拥有自己的智慧店铺”的愿景,持续赋能更多企业玩转智慧经营。即速应用旗下拥有“小程序搭建工具-即速应用”、“私域流量专家-即客云”等产品,帮助商家打通互联网全生态营销闭环。 -

即客云2.0重磅更新,让微信小程序运营更简单!
即客云作为一款基于企业微信的第三方工具,现从多维度提供超过30种功能,自上线以来,已服务多家企业,受到一致好评。近期,我们根据客户反馈和市场调研正式推出升级版 即客云2.0!更新了私域运营SOP,群日历功能,批量拓客,客户雷达,消息推送,个人欢迎语,帮助企业更好运用企业微信;同时提升了社群运营工作标准化,提升运营效率,帮助企业实现客户增长,玩转私域流量。 -

零代码 + AI 双轮驱动|即速应用解锁人工智能小程序开发新范式
无需代码、无需 AI 算法功底,普通人也能快速搭建智能小程序。即速应用将人工智能与零代码开发深度融合,推出 AI 智能生成能力,用户通过自然语言描述需求,AI 自动生成小程序页面、功能模块与后台配置,覆盖商城、预约、同城、社区团购等全场景。平台内置 AI 智能推荐、智能客服、用户画像分析等能力,一键对接微信生态,打通视频号、企业微信、短信跳转,帮企业快速落地 AI 应用,抢占智慧经营先机,让每家企业都拥有 AI 驱动的智慧店铺。










