前言背景
平時(shí)開發(fā)中遇到根據(jù)當(dāng)前用戶的角色,只能查看數(shù)據(jù)權(quán)限范圍的數(shù)據(jù)需求。列表實(shí)現(xiàn)方案有兩種,一是在開發(fā)初期就做好判斷賽選,但如果這個(gè)需求是中途加的,或不希望每個(gè)接口都加一遍,就可以方案二加攔截器的方式。在mybatis執(zhí)行sql前修改語句,限定where范圍。
當(dāng)然攔截器生效后是全局性的,如何保證只對(duì)需要的接口進(jìn)行攔截和轉(zhuǎn)化,就可以應(yīng)用注解進(jìn)行識(shí)別
因此具體需要哪些步驟就明確了
創(chuàng)建注解類
創(chuàng)建攔截器實(shí)現(xiàn)InnerInterceptor接口,重寫查詢方法
創(chuàng)建處理類,獲取數(shù)據(jù)權(quán)限 SQL 片段,設(shè)置where
將攔截器加到MyBatis-Plus插件中
自定義注解
importjava.lang.annotation.ElementType; importjava.lang.annotation.Retention; importjava.lang.annotation.RetentionPolicy; importjava.lang.annotation.Target; @Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public@interfaceUserDataPermission{ }
攔截器
importcom.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper; importcom.baomidou.mybatisplus.core.toolkit.PluginUtils; importcom.baomidou.mybatisplus.extension.parser.JsqlParserSupport; importcom.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; importlombok.*; importnet.sf.jsqlparser.expression.Expression; importnet.sf.jsqlparser.statement.select.PlainSelect; importnet.sf.jsqlparser.statement.select.Select; importnet.sf.jsqlparser.statement.select.SelectBody; importnet.sf.jsqlparser.statement.select.SetOperationList; importorg.apache.ibatis.executor.Executor; importorg.apache.ibatis.mapping.BoundSql; importorg.apache.ibatis.mapping.MappedStatement; importorg.apache.ibatis.session.ResultHandler; importorg.apache.ibatis.session.RowBounds; importjava.sql.SQLException; importjava.util.List; @Data @NoArgsConstructor @AllArgsConstructor @ToString(callSuper=true) @EqualsAndHashCode(callSuper=true) publicclassMyDataPermissionInterceptorextendsJsqlParserSupportimplementsInnerInterceptor{ /** *數(shù)據(jù)權(quán)限處理器 */ privateMyDataPermissionHandlerdataPermissionHandler; @Override publicvoidbeforeQuery(Executorexecutor,MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler,BoundSqlboundSql)throwsSQLException{ if(InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())){ return; } PluginUtils.MPBoundSqlmpBs=PluginUtils.mpBoundSql(boundSql); mpBs.sql(this.parserSingle(mpBs.sql(),ms.getId())); } @Override protectedvoidprocessSelect(Selectselect,intindex,Stringsql,Objectobj){ SelectBodyselectBody=select.getSelectBody(); if(selectBodyinstanceofPlainSelect){ this.setWhere((PlainSelect)selectBody,(String)obj); }elseif(selectBodyinstanceofSetOperationList){ SetOperationListsetOperationList=(SetOperationList)selectBody; ListselectBodyList=setOperationList.getSelects(); selectBodyList.forEach(s->this.setWhere((PlainSelect)s,(String)obj)); } } /** *設(shè)置where條件 * *@paramplainSelect查詢對(duì)象 *@paramwhereSegment查詢條件片段 */ privatevoidsetWhere(PlainSelectplainSelect,StringwhereSegment){ ExpressionsqlSegment=this.dataPermissionHandler.getSqlSegment(plainSelect,whereSegment); if(null!=sqlSegment){ plainSelect.setWhere(sqlSegment); } } }
攔截器處理器
基礎(chǔ)只涉及 = 表達(dá)式,要查詢集合范圍 in 看進(jìn)階版用例
importcn.hutool.core.collection.CollectionUtil; importlombok.SneakyThrows; importlombok.extern.slf4j.Slf4j; importnet.sf.jsqlparser.expression.Alias; importnet.sf.jsqlparser.expression.Expression; importnet.sf.jsqlparser.expression.HexValue; importnet.sf.jsqlparser.expression.StringValue; importnet.sf.jsqlparser.expression.operators.conditional.AndExpression; importnet.sf.jsqlparser.expression.operators.relational.EqualsTo; importnet.sf.jsqlparser.expression.operators.relational.ExpressionList; importnet.sf.jsqlparser.expression.operators.relational.InExpression; importnet.sf.jsqlparser.expression.operators.relational.ItemsList; importnet.sf.jsqlparser.schema.Column; importnet.sf.jsqlparser.schema.Table; importnet.sf.jsqlparser.statement.select.PlainSelect; importjava.lang.reflect.Method; importjava.util.List; importjava.util.Objects; importjava.util.Set; importjava.util.stream.Collectors; @Slf4j publicclassMyDataPermissionHandler{ /** *獲取數(shù)據(jù)權(quán)限SQL片段 * *@paramplainSelect查詢對(duì)象 *@paramwhereSegment查詢條件片段 *@returnJSqlParser條件表達(dá)式 */ @SneakyThrows(Exception.class) publicExpressiongetSqlSegment(PlainSelectplainSelect,StringwhereSegment){ //待執(zhí)行SQLWhere條件表達(dá)式 Expressionwhere=plainSelect.getWhere(); if(where==null){ where=newHexValue("1=1"); } log.info("開始進(jìn)行權(quán)限過濾,where:{},mappedStatementId:{}",where,whereSegment); //獲取mapper名稱 StringclassName=whereSegment.substring(0,whereSegment.lastIndexOf(".")); //獲取方法名 StringmethodName=whereSegment.substring(whereSegment.lastIndexOf(".")+1); TablefromItem=(Table)plainSelect.getFromItem(); //有別名用別名,無別名用表名,防止字段沖突報(bào)錯(cuò) AliasfromItemAlias=fromItem.getAlias(); StringmainTableName=fromItemAlias==null?fromItem.getName():fromItemAlias.getName(); //獲取當(dāng)前mapper的方法 Method[]methods=Class.forName(className).getMethods(); //遍歷判斷mapper的所以方法,判斷方法上是否有UserDataPermission for(Methodm:methods){ if(Objects.equals(m.getName(),methodName)){ UserDataPermissionannotation=m.getAnnotation(UserDataPermission.class); if(annotation==null){ returnwhere; } //1、當(dāng)前用戶Code Useruser=SecurityUtils.getUser(); //查看自己的數(shù)據(jù) //=表達(dá)式 EqualsTousesEqualsTo=newEqualsTo(); usesEqualsTo.setLeftExpression(newColumn(mainTableName+".creator_code")); usesEqualsTo.setRightExpression(newStringValue(user.getUserCode())); returnnewAndExpression(where,usesEqualsTo); } } //說明無權(quán)查看, where=newHexValue("1=2"); returnwhere; } }
將攔截器加到MyBatis-Plus插件中
如果你之前項(xiàng)目配插件 ,直接用下面方式就行
@Bean publicMybatisPlusInterceptormybatisPlusInterceptor(){ MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor(); //添加數(shù)據(jù)權(quán)限插件 MyDataPermissionInterceptordataPermissionInterceptor=newMyDataPermissionInterceptor(); //添加自定義的數(shù)據(jù)權(quán)限處理器 dataPermissionInterceptor.setDataPermissionHandler(newMyDataPermissionHandler()); interceptor.addInnerInterceptor(dataPermissionInterceptor); interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.MYSQL)); returninterceptor; }
但如果你項(xiàng)目之前是依賴包依賴,或有公司內(nèi)部統(tǒng)一攔截設(shè)置好,也可以往MybatisPlusInterceptor進(jìn)行插入,避免影響原有項(xiàng)目配置
@Bean publicMyDataPermissionInterceptormyInterceptor(MybatisPlusInterceptormybatisPlusInterceptor){ MyDataPermissionInterceptorsql=newMyDataPermissionInterceptor(); sql.setDataPermissionHandler(newMyDataPermissionHandler()); Listlist=newArrayList<>(); //添加數(shù)據(jù)權(quán)限插件 list.add(sql); //分頁插件 mybatisPlusInterceptor.setInterceptors(list); list.add(newPaginationInnerInterceptor(DbType.MYSQL)); returnsql; }
以上就是簡單版的是攔截器修改語句使用
使用方式
在mapper層添加注解即可
@UserDataPermission ListselectAllCustomerPage(IPage page,@Param("customerName")StringcustomerName);
基礎(chǔ)班只是能用,業(yè)務(wù)功能沒有特別約束,先保證能跑起來
進(jìn)階版 解決兩個(gè)問題:
加了角色,用角色決定范圍
解決不是mapper層自定義sql查詢問題。
兩個(gè)是完全獨(dú)立的問題 ,可根據(jù)情況分開解決
解決不是mapper層自定義sql查詢問題。
例如我們名稱簡單的sql語句 直接在Service層用mybatisPluse自帶的方法
xxxxService.list(WrapperqueryWrapper) xxxxService.page(newPage<>(),Wrapper queryWrapper)
以上這種我應(yīng)該把注解加哪里呢
因?yàn)閟ervice層,本質(zhì)上還是調(diào)mapper層, 所以還是在mapper層做文章,原來的mapper實(shí)現(xiàn)了extends BaseMapper 接口,所以能夠查詢,我們要做的就是在 mapper層中間套一個(gè)中間接口,來方便我們加注解
xxxxxMapper——》DataPermissionMapper(中間)——》BaseMapper
根據(jù)自身需要,在重寫的接口方法上加注解即可,這樣就影響原先的代碼
importcom.baomidou.mybatisplus.core.conditions.Wrapper; importcom.baomidou.mybatisplus.core.mapper.BaseMapper; importcom.baomidou.mybatisplus.core.metadata.IPage; importcom.baomidou.mybatisplus.core.toolkit.Constants; importorg.apache.ibatis.annotations.Param; importjava.io.Serializable; importjava.util.Collection; importjava.util.List; importjava.util.Map; publicinterfaceDataPermissionMapperextendsBaseMapper { /** *根據(jù)ID查詢 * *@paramid主鍵ID */ @Override @UserDataPermission TselectById(Serializableid); /** *查詢(根據(jù)ID批量查詢) * *@paramidList主鍵ID列表(不能為null以及empty) */ @Override @UserDataPermission List selectBatchIds(@Param(Constants.COLLECTION)Collection?extends?Serializable>idList); /** *查詢(根據(jù)columnMap條件) * *@paramcolumnMap表字段map對(duì)象 */ @Override @UserDataPermission List selectByMap(@Param(Constants.COLUMN_MAP)Map columnMap); /** *根據(jù)entity條件,查詢一條記錄 * *@paramqueryWrapper實(shí)體對(duì)象封裝操作類(可以為null) */ @Override @UserDataPermission TselectOne(@Param(Constants.WRAPPER)Wrapper queryWrapper); /** *根據(jù)Wrapper條件,查詢總記錄數(shù) * *@paramqueryWrapper實(shí)體對(duì)象封裝操作類(可以為null) */ @Override @UserDataPermission IntegerselectCount(@Param(Constants.WRAPPER)Wrapper queryWrapper); /** *根據(jù)entity條件,查詢?nèi)坑涗?* *@paramqueryWrapper實(shí)體對(duì)象封裝操作類(可以為null) */ @Override @UserDataPermission List selectList(@Param(Constants.WRAPPER)Wrapper queryWrapper); /** *根據(jù)Wrapper條件,查詢?nèi)坑涗?* *@paramqueryWrapper實(shí)體對(duì)象封裝操作類(可以為null) */ @Override @UserDataPermission List
解決角色控制查詢范圍
引入角色,我們先假設(shè)有三種角色,按照常規(guī)的業(yè)務(wù)需求,一種是管理員查看全部、一種是部門管理查看本部門、一種是僅查看自己。
有了以上假設(shè),就可以設(shè)置枚舉類編寫業(yè)務(wù)邏輯, 對(duì)是業(yè)務(wù)邏輯,所以我們只需要更改”攔截器處理器類“
建立范圍枚舉
建立角色枚舉以及范圍關(guān)聯(lián)關(guān)系
重寫攔截器處理方法
范圍枚舉
@AllArgsConstructor @Getter publicenumDataScope{ //Scope數(shù)據(jù)權(quán)限范圍:ALL(全部)、DEPT(部門)、MYSELF(自己) ALL("ALL"), DEPT("DEPT"), MYSELF("MYSELF"); privateStringname; }
角色枚舉
@AllArgsConstructor @Getter publicenumDataPermission{ //枚舉類型根據(jù)范圍從前往后排列,避免影響getScope //Scope數(shù)據(jù)權(quán)限范圍:ALL(全部)、DEPT(部門)、MYSELF(自己) DATA_MANAGER("數(shù)據(jù)管理員","DATA_MANAGER",DataScope.ALL), DATA_AUDITOR("數(shù)據(jù)審核員","DATA_AUDITOR",DataScope.DEPT), DATA_OPERATOR("數(shù)據(jù)業(yè)務(wù)員","DATA_OPERATOR",DataScope.MYSELF); privateStringname; privateStringcode; privateDataScopescope; publicstaticStringgetName(Stringcode){ for(DataPermissiontype:DataPermission.values()){ if(type.getCode().equals(code)){ returntype.getName(); } } returnnull; } publicstaticStringgetCode(Stringname){ for(DataPermissiontype:DataPermission.values()){ if(type.getName().equals(name)){ returntype.getCode(); } } returnnull; } publicstaticDataScopegetScope(Collectioncode){ for(DataPermissiontype:DataPermission.values()){ for(Stringv:code){ if(type.getCode().equals(v)){ returntype.getScope(); } } } returnDataScope.MYSELF; } }
重寫攔截器處理類 MyDataPermissionHandler
importlombok.SneakyThrows; importlombok.extern.slf4j.Slf4j; importnet.sf.jsqlparser.expression.Alias; importnet.sf.jsqlparser.expression.Expression; importnet.sf.jsqlparser.expression.HexValue; importnet.sf.jsqlparser.expression.StringValue; importnet.sf.jsqlparser.expression.operators.conditional.AndExpression; importnet.sf.jsqlparser.expression.operators.relational.EqualsTo; importnet.sf.jsqlparser.expression.operators.relational.ExpressionList; importnet.sf.jsqlparser.expression.operators.relational.InExpression; importnet.sf.jsqlparser.expression.operators.relational.ItemsList; importnet.sf.jsqlparser.schema.Column; importnet.sf.jsqlparser.schema.Table; importnet.sf.jsqlparser.statement.select.PlainSelect; importjava.lang.reflect.Method; importjava.util.List; importjava.util.Objects; importjava.util.Set; importjava.util.stream.Collectors; @Slf4j publicclassMyDataPermissionHandler{ privateRemoteRoleServiceremoteRoleService; privateRemoteUserServiceremoteUserService; /** *獲取數(shù)據(jù)權(quán)限SQL片段 * *@paramplainSelect查詢對(duì)象 *@paramwhereSegment查詢條件片段 *@returnJSqlParser條件表達(dá)式 */ @SneakyThrows(Exception.class) publicExpressiongetSqlSegment(PlainSelectplainSelect,StringwhereSegment){ remoteRoleService=SpringUtil.getBean(RemoteRoleService.class); remoteUserService=SpringUtil.getBean(RemoteUserService.class); //待執(zhí)行SQLWhere條件表達(dá)式 Expressionwhere=plainSelect.getWhere(); if(where==null){ where=newHexValue("1=1"); } log.info("開始進(jìn)行權(quán)限過濾,where:{},mappedStatementId:{}",where,whereSegment); //獲取mapper名稱 StringclassName=whereSegment.substring(0,whereSegment.lastIndexOf(".")); //獲取方法名 StringmethodName=whereSegment.substring(whereSegment.lastIndexOf(".")+1); TablefromItem=(Table)plainSelect.getFromItem(); //有別名用別名,無別名用表名,防止字段沖突報(bào)錯(cuò) AliasfromItemAlias=fromItem.getAlias(); StringmainTableName=fromItemAlias==null?fromItem.getName():fromItemAlias.getName(); //獲取當(dāng)前mapper的方法 Method[]methods=Class.forName(className).getMethods(); //遍歷判斷mapper的所以方法,判斷方法上是否有UserDataPermission for(Methodm:methods){ if(Objects.equals(m.getName(),methodName)){ UserDataPermissionannotation=m.getAnnotation(UserDataPermission.class); if(annotation==null){ returnwhere; } //1、當(dāng)前用戶Code Useruser=SecurityUtils.getUser(); //2、當(dāng)前角色即角色或角色類型(可能多種角色) SetroleTypeSet=remoteRoleService.currentUserRoleType(); DataScopescopeType=DataPermission.getScope(roleTypeSet); switch(scopeType){ //查看全部 caseALL: returnwhere; caseDEPT: //查看本部門用戶數(shù)據(jù) //創(chuàng)建IN表達(dá)式 //創(chuàng)建IN范圍的元素集合 List deptUserList=remoteUserService.listUserCodesByDeptCodes(user.getDeptCode()); //把集合轉(zhuǎn)變?yōu)镴SQLParser需要的元素列表 ItemsListdeptList=newExpressionList(deptUserList.stream().map(StringValue::new).collect(Collectors.toList())); InExpressioninExpressiondept=newInExpression(newColumn(mainTableName+".creator_code"),deptList); returnnewAndExpression(where,inExpressiondept); caseMYSELF: //查看自己的數(shù)據(jù) //=表達(dá)式 EqualsTousesEqualsTo=newEqualsTo(); usesEqualsTo.setLeftExpression(newColumn(mainTableName+".creator_code")); usesEqualsTo.setRightExpression(newStringValue(user.getUserCode())); returnnewAndExpression(where,usesEqualsTo); default: break; } } } //說明無權(quán)查看, where=newHexValue("1=2"); returnwhere; } }
以上就是全篇知識(shí)點(diǎn), 需要注意的點(diǎn)可能有:
記得把攔截器加到MyBatis-Plus的插件中,確保生效
要有一個(gè)業(yè)務(wù)賽選標(biāo)識(shí)字段, 這里用的創(chuàng)建人 creator_code, 也可以用dept_code 等等。
審核編輯:劉清
-
處理器
+關(guān)注
關(guān)注
68文章
19034瀏覽量
228450 -
SQL
+關(guān)注
關(guān)注
1文章
751瀏覽量
43985
原文標(biāo)題:巧用 MyBatis Plus 實(shí)現(xiàn)數(shù)據(jù)權(quán)限控制
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論