MyBatis的動態(tài)攔截sql并修改
需求
因工作需求,需要根據(jù)用戶的數(shù)據(jù)權(quán)限,來查詢并展示相應(yīng)的數(shù)據(jù),那么就需要動態(tài)攔截sql,在根據(jù)用戶權(quán)限做相應(yīng)的處理,因此需要一個通用攔截器,并以注解實(shí)現(xiàn)。該文只做查詢攔截,如有其他需求,可根據(jù)工作做相應(yīng)更改。
步驟一
該注解是方法級,因此需要注解在dao層方法上,如有需要也可更改為類級
注解:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented // 指名數(shù)據(jù)庫查詢方法需要和權(quán)限掛鉤 public @interface Permission { }
步驟二
定義攔截器實(shí)現(xiàn)接口重寫其intercept方法
@Intercepts({ // @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) // @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}), @Signature(type = Executor.class, method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}) }) @Component public class PermissionInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { } }
步驟三
拿到所有查詢sql請求,并得到相應(yīng)的statement
@Intercepts({ // @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) // @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}), @Signature(type = Executor.class, method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}) }) @Component public class PermissionInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { String processSql = ExecutorPluginUtils.getSqlByInvocation(invocation); // 執(zhí)行自定義修改sql操作 // 獲取sql String sql2Reset = processSql; Statement statement = CCJSqlParserUtil.parse(processSql); MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; } }
步驟四
如果后端未用分頁,則這步可以省略在項(xiàng)目啟動類下完成該配置
//得到spring上下文 ConfigurableApplicationContext run = SpringApplication.run(Application.class, args); Interceptor permissionInterceptor = (Interceptor) run.getBean("permissionInterceptor"); //這種方式添加mybatis攔截器保證在pageHelper前執(zhí)行 run.getBean(SqlSessionFactory.class).getConfiguration().addInterceptor(permissionInterceptor);
步驟五
工具類
package com.ydy.common.utils; import com.ydy.common.annotation.Permission; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.reflection.DefaultReflectorFactory; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.factory.DefaultObjectFactory; import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.sql.SQLException; import java.util.Arrays; import java.util.Objects; public class ExecutorPluginUtils { /** * 獲取sql語句 * @param invocation * @return */ public static String getSqlByInvocation(Invocation invocation) { final Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; Object parameterObject = args[1]; BoundSql boundSql = ms.getBoundSql(parameterObject); return boundSql.getSql(); } /** * 包裝sql后,重置到invocation中 * @param invocation * @param sql * @throws SQLException */ public static void resetSql2Invocation(Invocation invocation, String sql) throws SQLException { final Object[] args = invocation.getArgs(); MappedStatement statement = (MappedStatement) args[0]; Object parameterObject = args[1]; BoundSql boundSql = statement.getBoundSql(parameterObject); MappedStatement newStatement = newMappedStatement(statement, new BoundSqlSqlSource(boundSql)); MetaObject msObject = MetaObject.forObject(newStatement, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(),new DefaultReflectorFactory()); msObject.setValue("sqlSource.boundSql.sql", sql); args[0] = newStatement; } private static MappedStatement newMappedStatement(MappedStatement ms, SqlSource newSqlSource) { MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType()); builder.resource(ms.getResource()); builder.fetchSize(ms.getFetchSize()); builder.statementType(ms.getStatementType()); builder.keyGenerator(ms.getKeyGenerator()); if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) { StringBuilder keyProperties = new StringBuilder(); for (String keyProperty : ms.getKeyProperties()) { keyProperties.append(keyProperty).append(","); } keyProperties.delete(keyProperties.length() - 1, keyProperties.length()); builder.keyProperty(keyProperties.toString()); } builder.timeout(ms.getTimeout()); builder.parameterMap(ms.getParameterMap()); builder.resultMaps(ms.getResultMaps()); builder.resultSetType(ms.getResultSetType()); builder.cache(ms.getCache()); builder.flushCacheRequired(ms.isFlushCacheRequired()); builder.useCache(ms.isUseCache()); return builder.build(); } /** * 是否標(biāo)記為區(qū)域字段 * @return */ public static boolean isAreaTag( MappedStatement mappedStatement) throws ClassNotFoundException { String id = mappedStatement.getId(); //獲取類名 String className = id.substring(0, id.lastIndexOf(".")); Class clazz = Class.forName(className); //獲取方法名 String methodName = id.substring(id.lastIndexOf(".") + 1); //這里是博主工作需求,防止pagehelper那里未生效 if(methodName.contains("_COUNT")){ methodName=methodName.replace("_COUNT",""); } String m=methodName; Class<?> classType = Class.forName(id.substring(0,mappedStatement.getId().lastIndexOf("."))); //獲取對應(yīng)攔截方法名 String mName = mappedStatement.getId().substring(mappedStatement.getId().lastIndexOf(".") + 1); //這里是博主工作需求,防止pagehelper那里未生效 if(mName.contains("_COUNT")){ mName=mName.replace("_COUNT",""); } boolean ignore = false; //獲取該類(接口)的所有方法,如果你查詢的方法就寫在該類,就不需要下面的if判斷 Method[] declaredMethods = classType.getDeclaredMethods(); Method declaredMethod = Arrays.stream(declaredMethods).filter(it -> it.getName().equals(m)).findFirst().orElse(null); //該判斷是拿到該接口的超類的方法,博主的查詢方法就在超類里,因此需要利用下面代碼來獲取對應(yīng)方法 if (declaredMethod == null) { Type[] genericInterfaces = clazz.getGenericInterfaces(); declaredMethod = Arrays.stream(genericInterfaces).map(e -> { Method[] declaredMethods1 = ((Class) e).getDeclaredMethods(); return Arrays.stream(declaredMethods1).filter(it -> it.getName().equals(m)).findFirst().orElse(null); }).filter(Objects::nonNull).findFirst().orElse(null); } if(declaredMethod!=null){ //查詢方法是否被permission標(biāo)記注解 ignore = declaredMethod.isAnnotationPresent(Permission.class); } return ignore; } /** * 是否標(biāo)記為區(qū)域字段 * @return */ public static boolean isAreaTagIngore( MappedStatement mappedStatement) throws ClassNotFoundException { String id = mappedStatement.getId(); String className = id.substring(0, id.lastIndexOf(".")); Class clazz = Class.forName(className); String methodName = id.substring(id.lastIndexOf(".") + 1); Class<?> classType = Class.forName(id.substring(0,mappedStatement.getId().lastIndexOf("."))); //獲取對應(yīng)攔截方法名 String mName = mappedStatement.getId().substring(mappedStatement.getId().lastIndexOf(".") + 1); boolean ignore = false; Method[] declaredMethods = classType.getDeclaredMethods(); Method declaredMethod = Arrays.stream(declaredMethods).filter(it -> it.getName().equals(methodName)).findFirst().orElse(null); if (declaredMethod == null) { Type[] genericInterfaces = clazz.getGenericInterfaces(); declaredMethod = Arrays.stream(genericInterfaces).map(e -> { Method[] declaredMethods1 = ((Class) e).getDeclaredMethods(); return Arrays.stream(declaredMethods1).filter(it -> it.getName().equals(methodName)).findFirst().orElse(null); }).filter(Objects::nonNull).findFirst().orElse(null); } ignore = declaredMethod.isAnnotationPresent(Permission.class); return ignore; } public static String getOperateType(Invocation invocation) { final Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; SqlCommandType commondType = ms.getSqlCommandType(); if (commondType.compareTo(SqlCommandType.SELECT) == 0) { return "select"; } return null; } // 定義一個內(nèi)部輔助類,作用是包裝sq static class BoundSqlSqlSource implements SqlSource { private BoundSql boundSql; public BoundSqlSqlSource(BoundSql boundSql) { this.boundSql = boundSql; } @Override public BoundSql getBoundSql(Object parameterObject) { return boundSql; } } }
步驟六
如果方法被permission注解進(jìn)入if方法,查詢各自數(shù)據(jù)權(quán)限,拼接sql,替換sql。如未進(jìn)入則放行。
if (ExecutorPluginUtils.isAreaTag(mappedStatement)) { //獲取該用戶所具有的角色的數(shù)據(jù)權(quán)限dataScope //因數(shù)據(jù)敏感省略 //獲取該用戶的所在公司或部門下的所有人 //例如 StringBuffer orgBuffer = new StringBuffer(); // orgBuffer.append("("); //String collect = allUserByOrgs.stream().map(String::valueOf).collect(Collectors.joining(",")); //orgBuffer.append(collect).append(")"); //String orgsUser = orgBuffer.toString(); try { if (statement instanceof Select) { Select selectStatement = (Select) statement; //其中的PlainSelect 可以拿到sql語句的全部節(jié)點(diǎn)信息,具體各位可以看源碼 PlainSelect plain = (PlainSelect) selectStatement.getSelectBody(); //獲取所有外連接 List<Join> joins = plain.getJoins(); //獲取到原始sql語句 String sql = processSql; StringBuffer whereSql = new StringBuffer(); switch (dataScope) { //這里dataScope 范圍 1 所有數(shù)據(jù)權(quán)限 2 本人 3,部門及分部門(遞歸) 4.公司及分公司(遞歸) //所有數(shù)據(jù)權(quán)限作用在人上,因此sql用 in case 1: whereSql.append("1=1"); break; case 2: for (Join join : joins) { Table rightItem = (Table) join.getRightItem(); //匹配表名 if(rightItem.getName().equals("sec_user")){ //獲取別名 if(rightItem.getAlias()!=null){ whereSql.append(rightItem.getAlias().getName()).append(".id = ").append(SecurityUtils.getLoginUser().getId()); }else { whereSql.append("id = ").append(deptsUser); } } } break; case 3: for (Join join : joins) { Table rightItem = (Table) join.getRightItem(); if(rightItem.getName().equals("sec_user")){ if(rightItem.getAlias()!=null){ whereSql.append(rightItem.getAlias().getName()).append(".id in ").append(deptsUser); }else { whereSql.append("id in ").append(deptsUser); } } } break; case 4: for (Join join : joins) { Table rightItem = (Table) join.getRightItem(); if(rightItem.getName().equals("sec_user")){ if(rightItem.getAlias()!=null){ whereSql.append(rightItem.getAlias().getName()).append(".id in ").append(orgsUser); }else { whereSql.append("id in ").append(deptsUser); } } } break; } //獲取where節(jié)點(diǎn) Expression where = plain.getWhere(); if (where == null) { if (whereSql.length() > 0) { Expression expression = CCJSqlParserUtil .parseCondExpression(whereSql.toString()); Expression whereExpression = (Expression) expression; plain.setWhere(whereExpression); } } else { if (whereSql.length() > 0) { //where條件之前存在,需要重新進(jìn)行拼接 whereSql.append(" and ( " + where.toString() + " )"); } else { //新增片段不存在,使用之前的sql whereSql.append(where.toString()); } Expression expression = CCJSqlParserUtil .parseCondExpression(whereSql.toString()); plain.setWhere(expression); } sql2Reset = selectStatement.toString(); } } catch (Exception e) { e.printStackTrace(); } } // 替換sql ExecutorPluginUtils.resetSql2Invocation(invocation, sql2Reset); //放行 Object proceed = invocation.proceed(); return proceed;
到此這篇關(guān)于MyBatis的動態(tài)攔截sql并修改的文章就介紹到這了,更多相關(guān)MyBatis動態(tài)攔截sql內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- MyBatis中動態(tài)SQL的使用指南
- MyBatis中實(shí)現(xiàn)動態(tài)SQL標(biāo)簽
- 使用MyBatis的動態(tài)SQL注解實(shí)現(xiàn)實(shí)體的CRUD操作代碼
- MyBatis實(shí)現(xiàn)動態(tài)SQL的方法
- Mybatis之動態(tài)SQL使用小結(jié)(全網(wǎng)最新)
- Mybatis動態(tài)Sql標(biāo)簽使用小結(jié)
- MyBatis中的XML實(shí)現(xiàn)和動態(tài)SQL實(shí)現(xiàn)示例詳解
- MyBatis映射文件中的動態(tài)SQL實(shí)例詳解
- 詳解MyBatis特性之動態(tài)SQL
- Mybatis使用注解實(shí)現(xiàn)復(fù)雜動態(tài)SQL的方法詳解
- mybatis動態(tài)生成sql語句的實(shí)現(xiàn)示例
相關(guān)文章
Java String方法獲取字符出現(xiàn)次數(shù)及字符最大相同部分示例
這篇文章主要介紹了Java String方法獲取字符出現(xiàn)次數(shù)及字符最大相同部分,涉及java字符串的遍歷、比較、計(jì)算等相關(guān)操作技巧,需要的朋友可以參考下2017-09-09springboot跨域訪問cros與@CrossOrigin注解詳析
這篇文章主要給大家介紹了關(guān)于springboot跨域訪問cros與@CrossOrigin注解的相關(guān)資料,跨域是指不同域名之間相互訪問,它是瀏覽器的同源策略造成的,是瀏覽器對JavaScript施加的安全限制,需要的朋友可以參考下2023-12-12SpringBoot對接Twilio實(shí)現(xiàn)發(fā)送驗(yàn)證碼和驗(yàn)證短信碼
Twilio是一家提供云通信服務(wù)的公司,旨在幫助開發(fā)者和企業(yè)通過簡單的API實(shí)現(xiàn)各種通信功能,下面我們來看看如何對接Twilio實(shí)現(xiàn)發(fā)送驗(yàn)證碼和驗(yàn)證短信碼吧2025-03-03java線程安全鎖ReentrantReadWriteLock原理分析readLock
這篇文章主要為大家介紹了java線程安全鎖ReentrantReadWriteLock原理分析readLock,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10Java實(shí)現(xiàn)驗(yàn)證碼驗(yàn)證功能
Java如何實(shí)現(xiàn)驗(yàn)證碼驗(yàn)證功能呢?日常生活中,驗(yàn)證碼隨處可見,他可以在一定程度上保護(hù)賬號安全,那么他是怎么實(shí)現(xiàn)的呢?今天通過本文給大家實(shí)例詳解,需要的朋友參考下2017-02-02SSH結(jié)合jquery實(shí)現(xiàn)三級聯(lián)動效果
這篇文章主要為大家詳細(xì)介紹了SSH結(jié)合jquery實(shí)現(xiàn)三級聯(lián)動效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-01-01