使用MyBatis攔截器實(shí)現(xiàn)sql查詢權(quán)限動(dòng)態(tài)修改代碼實(shí)例
動(dòng)機(jī)和具體情景
最近考慮怎么在Mybatis自動(dòng)創(chuàng)建sql執(zhí)行過程中進(jìn)行介入,來不對原有代碼耦合的情況下,實(shí)現(xiàn)對sql的修改。
考慮情景,比如多部門管理系統(tǒng),員工工資和賬戶信息敏感,每個(gè)部門只能查到對應(yīng)權(quán)限的員工信息。為了實(shí)現(xiàn)sql的鑒權(quán),本來是需要將原始的sql語句加上某個(gè)權(quán)限字段的判斷。
為了不耦合,現(xiàn)在的方案是在需要鑒權(quán)的Mybatis Mapper方法上增加一個(gè)注解,在運(yùn)行過程中判斷該注解存在即對sql進(jìn)行修改,形成新的帶權(quán)限字段判斷的sql,這樣對原始代碼的修改就少很多(加個(gè)注解就行)。
基本原理和解析流程
Mybatis 允許在映射語句過程中的某一點(diǎn)進(jìn)行攔截調(diào)用,其提供了基于反射的攔截類 Interceptor 來對方法進(jìn)行攔截。
這些可攔截的方法存在的原始執(zhí)行類包括: Executor (執(zhí)行器相關(guān)), ParameterHandler (參數(shù)處理相關(guān)), ResultSetHandler (結(jié)果集相關(guān)), StatementHandler (sql語法和會(huì)話創(chuàng)建相關(guān))四種。
我們的需求是對sql語句進(jìn)行改寫,選擇對 StatementHandler 進(jìn)行改寫。
通過反射獲取到此次Mybatis執(zhí)行的原始Mapper接口和方法名,通過判斷我們的自定義注解 @permission 是否存在來選擇鑒權(quán)行為,之后從session拿到當(dāng)前查詢權(quán)限,從配置文件中拿到權(quán)限可查詢的數(shù)據(jù)范圍,即可對sql進(jìn)行修改。最后將修改后的sql反射注入回Mybatis的對應(yīng)執(zhí)行類即可。
具體實(shí)現(xiàn)
原始查詢代碼
AccountInfo 賬戶信息類
映射實(shí)體類,存放賬戶信息。要注意的是在數(shù)據(jù)庫表中還有一個(gè)字段permission表示查詢權(quán)限,在實(shí)體類中并沒有表示。
public class AccountInfo implements Serializable { int id; String account; String name; BigDecimal money; public AccountInfo(){} public AccountInfo(int id, String account, String name, BigDecimal money){ this.id = id; this.account = account; this.name = name; this.money = money; }
AccountMapper 操作Mapper類
@Permission 為自定義注解,表示該方法需要進(jìn)行鑒權(quán)操作,只能查詢當(dāng)前權(quán)限下對應(yīng)的數(shù)據(jù)信息。
@Mapper public interface AccountMapper { @Permission @Select("select * from account") public List<AccountInfo> getAccountInfoList(); }
MainController 主要控制類
給出了查詢接口,同時(shí)因?yàn)樵O(shè)定上權(quán)限是存在session中的,給了個(gè)模擬賦予權(quán)限的接口。
@Controller public class MainController { @Autowired AccountMapper accountMapper; @RequestMapping("/getAccountInfo") @ResponseBody public String getAccountInfo() throws JsonProcessingException { ObjectMapper objectMapper = new ObjectMapper(); List<AccountInfo> accounts = accountMapper.getAccountInfoList(); return objectMapper.writeValueAsString(accounts); } @RequestMapping("/setPermission") @ResponseBody public String setPermission(HttpServletRequest request, String permission){ request.getSession().setAttribute("permission", permission); return permission; } }
權(quán)限相關(guān)實(shí)現(xiàn)
配置文件設(shè)置
增加權(quán)限級(jí)聯(lián)的設(shè)置,為值鍵對形式,表示key能查詢的數(shù)據(jù)范圍。
permission: permissionMap: develop: "\"develop\"" advertise: "\"advertise\"" finance: "\"develop\", \"advertise\", \"finance\""
需要注意的是sql字符串查詢需要使用引號(hào),所以配置文件中需要增加引號(hào)轉(zhuǎn)義,方便后續(xù)sql的使用。
PermissionConfig讀取配置文件
Map形式不能直接讀取,增加對應(yīng)的讀取config類,主要其中的成員名稱需要和配置文件中對應(yīng)(這邊為permissionMap)。
// 從配置文件中讀取permission層次范圍 @Configuration @ConfigurationProperties(prefix = "permission") @EnableConfigurationProperties(PermissionConfig.class) public class PermissionConfig { private Map<String, String> permissionMap = new HashMap<>(); public Map<String, String> getPermissionMap() { return permissionMap; } public void setPermissionMap(Map<String, String> permissionMap) { this.permissionMap = permissionMap; } }
自定義注解
注解只是為了判斷是否需要鑒權(quán),不需要特殊的成員。同時(shí)設(shè)置 retention 為 runtime ,并設(shè)置作用對象為方法 method 。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) // 指名數(shù)據(jù)庫查詢方法需要和權(quán)限掛鉤 public @interface Permission {}
Mybatis攔截器實(shí)現(xiàn)
主要的實(shí)現(xiàn)集中在 PermissionInterceptor 中,主要可分為幾個(gè)部分:攔截配置,元數(shù)據(jù)獲取,自定義注解判斷,權(quán)限獲取與sql修改,反射注入。主要邏輯集中在 intercept 方法中。
攔截配置
主要是通過 @Intercepts 注解對攔截器類需要攔截的Handler和方法進(jìn)行設(shè)置,方便之后反射獲取對應(yīng)的類。我們這邊是對語句的最終sql進(jìn)行處理,選擇StatementHandler中的 prepare 方法進(jìn)行攔截,args中為方法參數(shù)類型來判斷重載。如果是對query進(jìn)行攔截,后續(xù)注入時(shí)其實(shí)已經(jīng)執(zhí)行了,新的sql并不會(huì)被調(diào)用。
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) }) @Component public class PermissionInterceptor implements Interceptor {
元數(shù)據(jù)獲取
用的Mybatis給的元數(shù)據(jù)類MetaObject進(jìn)行獲取。
private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory(); private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory(); private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory(); @Autowired private PermissionConfig permissionConfig; @Override public Object intercept(Invocation invocation) throws Throwable { // 獲取sql信息 StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); String sql = boundSql.getSql(); System.out.println("原sql為: " + sql); // 獲取元數(shù)據(jù) MetaObject metaResultSetHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, REFLECTOR_FACTORY); MappedStatement mappedStatement = (MappedStatement) metaResultSetHandler.getValue("delegate.mappedStatement");
MappedStatement 在 對應(yīng)的Handler 的 delegate.mappedStatement 屬性對象中,包含元數(shù)據(jù)信息。 獲取類和方法信息。
// 獲取調(diào)用方法 String id = mappedStatement.getId(); String className = id.substring(0, id.lastIndexOf(".")); String methodName = id.substring(id.lastIndexOf(".") + 1); System.out.println("調(diào)用方法為: " + id);
注解判斷
反射獲取對應(yīng)方法的注解列表即可。
// 注解查詢 Class clazz = Class.forName(className); Method method = clazz.getDeclaredMethod(methodName); boolean needPermission = method.isAnnotationPresent(Permission.class); // 對注解方法進(jìn)行權(quán)限處理 if(needPermission){ System.out.println("需要進(jìn)行sql權(quán)限變化");
獲取權(quán)限信息并進(jìn)行sql修改
配置文件中獲取權(quán)限范圍信息,增加到原sql的條件判斷中。
// 獲取權(quán)限信息 HttpSession session = HttpUtil.getSession(); String permission = (String) session.getAttribute("permission"); Map<String, String> map = permissionConfig.getPermissionMap(); for(String key:map.keySet()){ System.out.println(key); } String canSelectPermission = null; if(map.containsKey(permission)){ canSelectPermission = map.get(permission); } System.out.printf("當(dāng)前權(quán)限:%s, 可查詢范圍:%s%n", permission, canSelectPermission); // 修改sql String newSql = String.format("select * from (%s) `range` where `range`.permission in (%s)", sql, canSelectPermission); // String newSql = "select * from account where permission in (\"advertise\")"; System.out.println("修改后的sql為: " + newSql);
反射注入并執(zhí)行
注入到BoundSql類中,替換原sql。
// 反射修改handler中的sql以執(zhí)行 Class boundClass = boundSql.getClass(); Field field = boundClass.getDeclaredField("sql"); field.setAccessible(true); field.set(boundSql, newSql);
效果展示
數(shù)據(jù)庫簡單數(shù)據(jù)
包括一個(gè)查詢權(quán)限字段
給予權(quán)限信息
Session中寫入權(quán)限。
不同權(quán)限下獲取的信息結(jié)果
Finance:
Develop:
總結(jié)
一個(gè)簡單的Mybatis的攔截器嘗試,用于對sql依靠查詢權(quán)限進(jìn)行動(dòng)態(tài)修改。
主要就是Mybatis這個(gè)MetaObject需要知道對應(yīng)的statement的value才能反射拿到,查了好久才發(fā)現(xiàn)是delegate.mappedStatement,很神秘。
到此這篇關(guān)于使用MyBatis攔截器實(shí)現(xiàn)sql查詢權(quán)限動(dòng)態(tài)修改代碼實(shí)例的文章就介紹到這了,更多相關(guān)MyBatis攔截器動(dòng)態(tài)修改sql內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot+springJdbc+postgresql 實(shí)現(xiàn)多數(shù)據(jù)源的配置
本文主要介紹了springboot+springJdbc+postgresql 實(shí)現(xiàn)多數(shù)據(jù)源的配置,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09在Java的Struts框架下進(jìn)行web編程的入門教程
這篇文章主要介紹了在Java的Struts框架下進(jìn)行web編程的入門教程,需要的朋友可以參考下2015-11-11spring在service層的方法報(bào)錯(cuò)事務(wù)不會(huì)回滾的解決
這篇文章主要介紹了spring在service層的方法報(bào)錯(cuò)事務(wù)不會(huì)回滾的解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02springboot 通過代碼自動(dòng)生成pid的方法
這篇文章主要介紹了springboot 通過代碼自動(dòng)生成pid的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-07-07最長公共子序列問題的深度分析與Java實(shí)現(xiàn)方式
本文詳細(xì)介紹了最長公共子序列(LCS)問題,包括其概念、暴力解法、動(dòng)態(tài)規(guī)劃解法,并提供了Java代碼實(shí)現(xiàn),暴力解法雖然簡單,但在大數(shù)據(jù)處理中效率較低,動(dòng)態(tài)規(guī)劃解法通過構(gòu)建DP表,顯著提高了計(jì)算效率,適用于大規(guī)模數(shù)據(jù)處理2025-02-02