前言背景
平時開發中遇到根據當前用戶的角色,只能查看數據權限范圍的數據需求。列表實現方案有兩種,一是在開發初期就做好判斷賽選,但如果這個需求是中途加的,或不希望每個接口都加一遍,就可以方案二加攔截器的方式。在mybatis執行sql前修改語句,限定where范圍。
當然攔截器生效后是全局性的,如何保證只對需要的接口進行攔截和轉化,就可以應用注解進行識別
因此具體需要哪些步驟就明確了
創建注解類
創建攔截器實現InnerInterceptor接口,重寫查詢方法
創建處理類,獲取數據權限 SQL 片段,設置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{ /** *數據權限處理器 */ 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)); } } /** *設置where條件 * *@paramplainSelect查詢對象 *@paramwhereSegment查詢條件片段 */ privatevoidsetWhere(PlainSelectplainSelect,StringwhereSegment){ ExpressionsqlSegment=this.dataPermissionHandler.getSqlSegment(plainSelect,whereSegment); if(null!=sqlSegment){ plainSelect.setWhere(sqlSegment); } } }
攔截器處理器
基礎只涉及 = 表達式,要查詢集合范圍 in 看進階版用例
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{ /** *獲取數據權限SQL片段 * *@paramplainSelect查詢對象 *@paramwhereSegment查詢條件片段 *@returnJSqlParser條件表達式 */ @SneakyThrows(Exception.class) publicExpressiongetSqlSegment(PlainSelectplainSelect,StringwhereSegment){ //待執行SQLWhere條件表達式 Expressionwhere=plainSelect.getWhere(); if(where==null){ where=newHexValue("1=1"); } log.info("開始進行權限過濾,where:{},mappedStatementId:{}",where,whereSegment); //獲取mapper名稱 StringclassName=whereSegment.substring(0,whereSegment.lastIndexOf(".")); //獲取方法名 StringmethodName=whereSegment.substring(whereSegment.lastIndexOf(".")+1); TablefromItem=(Table)plainSelect.getFromItem(); //有別名用別名,無別名用表名,防止字段沖突報錯 AliasfromItemAlias=fromItem.getAlias(); StringmainTableName=fromItemAlias==null?fromItem.getName():fromItemAlias.getName(); //獲取當前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、當前用戶Code Useruser=SecurityUtils.getUser(); //查看自己的數據 //=表達式 EqualsTousesEqualsTo=newEqualsTo(); usesEqualsTo.setLeftExpression(newColumn(mainTableName+".creator_code")); usesEqualsTo.setRightExpression(newStringValue(user.getUserCode())); returnnewAndExpression(where,usesEqualsTo); } } //說明無權查看, where=newHexValue("1=2"); returnwhere; } }
將攔截器加到MyBatis-Plus插件中
如果你之前項目配插件 ,直接用下面方式就行
@Bean publicMybatisPlusInterceptormybatisPlusInterceptor(){ MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor(); //添加數據權限插件 MyDataPermissionInterceptordataPermissionInterceptor=newMyDataPermissionInterceptor(); //添加自定義的數據權限處理器 dataPermissionInterceptor.setDataPermissionHandler(newMyDataPermissionHandler()); interceptor.addInnerInterceptor(dataPermissionInterceptor); interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.MYSQL)); returninterceptor; }
但如果你項目之前是依賴包依賴,或有公司內部統一攔截設置好,也可以往MybatisPlusInterceptor進行插入,避免影響原有項目配置
@Bean publicMyDataPermissionInterceptormyInterceptor(MybatisPlusInterceptormybatisPlusInterceptor){ MyDataPermissionInterceptorsql=newMyDataPermissionInterceptor(); sql.setDataPermissionHandler(newMyDataPermissionHandler()); Listlist=newArrayList<>(); //添加數據權限插件 list.add(sql); //分頁插件 mybatisPlusInterceptor.setInterceptors(list); list.add(newPaginationInnerInterceptor(DbType.MYSQL)); returnsql; }
以上就是簡單版的是攔截器修改語句使用
使用方式
在mapper層添加注解即可
@UserDataPermission ListselectAllCustomerPage(IPage page,@Param("customerName")StringcustomerName);
基礎班只是能用,業務功能沒有特別約束,先保證能跑起來
進階版 解決兩個問題:
加了角色,用角色決定范圍
解決不是mapper層自定義sql查詢問題。
兩個是完全獨立的問題 ,可根據情況分開解決
解決不是mapper層自定義sql查詢問題。
例如我們名稱簡單的sql語句 直接在Service層用mybatisPluse自帶的方法
xxxxService.list(WrapperqueryWrapper) xxxxService.page(newPage<>(),Wrapper queryWrapper)
以上這種我應該把注解加哪里呢
因為service層,本質上還是調mapper層, 所以還是在mapper層做文章,原來的mapper實現了extends BaseMapper 接口,所以能夠查詢,我們要做的就是在 mapper層中間套一個中間接口,來方便我們加注解
xxxxxMapper——》DataPermissionMapper(中間)——》BaseMapper
根據自身需要,在重寫的接口方法上加注解即可,這樣就影響原先的代碼
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 { /** *根據ID查詢 * *@paramid主鍵ID */ @Override @UserDataPermission TselectById(Serializableid); /** *查詢(根據ID批量查詢) * *@paramidList主鍵ID列表(不能為null以及empty) */ @Override @UserDataPermission List selectBatchIds(@Param(Constants.COLLECTION)Collection?extends?Serializable>idList); /** *查詢(根據columnMap條件) * *@paramcolumnMap表字段map對象 */ @Override @UserDataPermission List selectByMap(@Param(Constants.COLUMN_MAP)Map columnMap); /** *根據entity條件,查詢一條記錄 * *@paramqueryWrapper實體對象封裝操作類(可以為null) */ @Override @UserDataPermission TselectOne(@Param(Constants.WRAPPER)Wrapper queryWrapper); /** *根據Wrapper條件,查詢總記錄數 * *@paramqueryWrapper實體對象封裝操作類(可以為null) */ @Override @UserDataPermission IntegerselectCount(@Param(Constants.WRAPPER)Wrapper queryWrapper); /** *根據entity條件,查詢全部記錄 * *@paramqueryWrapper實體對象封裝操作類(可以為null) */ @Override @UserDataPermission List selectList(@Param(Constants.WRAPPER)Wrapper queryWrapper); /** *根據Wrapper條件,查詢全部記錄 * *@paramqueryWrapper實體對象封裝操作類(可以為null) */ @Override @UserDataPermission List
解決角色控制查詢范圍
引入角色,我們先假設有三種角色,按照常規的業務需求,一種是管理員查看全部、一種是部門管理查看本部門、一種是僅查看自己。
有了以上假設,就可以設置枚舉類編寫業務邏輯, 對是業務邏輯,所以我們只需要更改”攔截器處理器類“
建立范圍枚舉
建立角色枚舉以及范圍關聯關系
重寫攔截器處理方法
范圍枚舉
@AllArgsConstructor @Getter publicenumDataScope{ //Scope數據權限范圍:ALL(全部)、DEPT(部門)、MYSELF(自己) ALL("ALL"), DEPT("DEPT"), MYSELF("MYSELF"); privateStringname; }
角色枚舉
@AllArgsConstructor @Getter publicenumDataPermission{ //枚舉類型根據范圍從前往后排列,避免影響getScope //Scope數據權限范圍:ALL(全部)、DEPT(部門)、MYSELF(自己) DATA_MANAGER("數據管理員","DATA_MANAGER",DataScope.ALL), DATA_AUDITOR("數據審核員","DATA_AUDITOR",DataScope.DEPT), DATA_OPERATOR("數據業務員","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; /** *獲取數據權限SQL片段 * *@paramplainSelect查詢對象 *@paramwhereSegment查詢條件片段 *@returnJSqlParser條件表達式 */ @SneakyThrows(Exception.class) publicExpressiongetSqlSegment(PlainSelectplainSelect,StringwhereSegment){ remoteRoleService=SpringUtil.getBean(RemoteRoleService.class); remoteUserService=SpringUtil.getBean(RemoteUserService.class); //待執行SQLWhere條件表達式 Expressionwhere=plainSelect.getWhere(); if(where==null){ where=newHexValue("1=1"); } log.info("開始進行權限過濾,where:{},mappedStatementId:{}",where,whereSegment); //獲取mapper名稱 StringclassName=whereSegment.substring(0,whereSegment.lastIndexOf(".")); //獲取方法名 StringmethodName=whereSegment.substring(whereSegment.lastIndexOf(".")+1); TablefromItem=(Table)plainSelect.getFromItem(); //有別名用別名,無別名用表名,防止字段沖突報錯 AliasfromItemAlias=fromItem.getAlias(); StringmainTableName=fromItemAlias==null?fromItem.getName():fromItemAlias.getName(); //獲取當前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、當前用戶Code Useruser=SecurityUtils.getUser(); //2、當前角色即角色或角色類型(可能多種角色) SetroleTypeSet=remoteRoleService.currentUserRoleType(); DataScopescopeType=DataPermission.getScope(roleTypeSet); switch(scopeType){ //查看全部 caseALL: returnwhere; caseDEPT: //查看本部門用戶數據 //創建IN表達式 //創建IN范圍的元素集合 List deptUserList=remoteUserService.listUserCodesByDeptCodes(user.getDeptCode()); //把集合轉變為JSQLParser需要的元素列表 ItemsListdeptList=newExpressionList(deptUserList.stream().map(StringValue::new).collect(Collectors.toList())); InExpressioninExpressiondept=newInExpression(newColumn(mainTableName+".creator_code"),deptList); returnnewAndExpression(where,inExpressiondept); caseMYSELF: //查看自己的數據 //=表達式 EqualsTousesEqualsTo=newEqualsTo(); usesEqualsTo.setLeftExpression(newColumn(mainTableName+".creator_code")); usesEqualsTo.setRightExpression(newStringValue(user.getUserCode())); returnnewAndExpression(where,usesEqualsTo); default: break; } } } //說明無權查看, where=newHexValue("1=2"); returnwhere; } }
以上就是全篇知識點, 需要注意的點可能有:
記得把攔截器加到MyBatis-Plus的插件中,確保生效
要有一個業務賽選標識字段, 這里用的創建人 creator_code, 也可以用dept_code 等等。
審核編輯:劉清
-
處理器
+關注
關注
68文章
19167瀏覽量
229153 -
SQL
+關注
關注
1文章
760瀏覽量
44078
原文標題:巧用 MyBatis Plus 實現數據權限控制
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論