Java如何利用Mybatis進行數(shù)據(jù)權限控制詳解
前言
權限控制主要分為兩塊,認證(Authentication)與授權(Authorization)。認證之后確認了身份正確,業(yè)務系統(tǒng)就會進行授權,現(xiàn)在業(yè)界比較流行的模型就是RBAC(Role-Based Access Control)。RBAC包含為下面四個要素:用戶、角色、權限、資源。用戶是源頭,資源是目標,用戶綁定至角色,資源與權限關聯(lián),最終將角色與權限關聯(lián),就形成了比較完整靈活的權限控制模型。
資源是最終需要控制的標的物,但是我們在一個業(yè)務系統(tǒng)中要將哪些元素作為待控制的資源呢?我將系統(tǒng)中待控制的資源分為三類:
- URL訪問資源(接口以及網(wǎng)頁)
- 界面元素資源(增刪改查導入導出的按鈕,重要的業(yè)務數(shù)據(jù)展示與否等)
- 數(shù)據(jù)資源
現(xiàn)在業(yè)內(nèi)普遍的實現(xiàn)方案實際上很粗放,就是單純的“菜單控制”,通過菜單顯示與否來達到控制權限的目的。
我仔細分析過,現(xiàn)在大家做的平臺分為To C和To B兩種:
- To C一般不會有太多的復雜權限控制,甚至大部分連菜單控制都不用,全部都可以訪問。
- To B一般都不是開放的,只要做好認證關口,能夠進入系統(tǒng)的只有內(nèi)部員工。大部分企業(yè)內(nèi)部的員工互聯(lián)網(wǎng)知識有限,而且作為內(nèi)部員工不敢對系統(tǒng)進行破壞性的嘗試。
所以針對現(xiàn)在的情況,考慮成本與產(chǎn)出,大部分設計者也不愿意在權限上進行太多的研發(fā)力量。
菜單和界面元素一般都是由前端編碼配合存儲數(shù)據(jù)實現(xiàn),URL訪問資源的控制也有一些框架比如SpringSecurity,Shiro。
目前我還沒有找到過數(shù)據(jù)權限控制的框架或者方法,所以自己整理了一份。
數(shù)據(jù)權限控制原理
數(shù)據(jù)權限控制最終的效果是會要求在同一個數(shù)據(jù)請求方法中,根據(jù)不同的權限返回不同的數(shù)據(jù)集,而且無需并且不能由研發(fā)編碼控制。這樣大家的第一想法應該就是AOP,攔截所有的底層方法,加入過濾條件。這樣的方式兼容性較強,但是復雜程度也會更高。我們這套系統(tǒng)中,采用的是利用Mybatis的plugin機制,在底層SQL解析時替換增加過濾條件。
這樣一套控制機制存在很明顯的優(yōu)缺點,首先缺點:
- 適用性有限,基于底層的Mybatis。
- 方言有限,針對了某種數(shù)據(jù)庫(我們使用Mysql),而且由于需要在底層解析處理條件所以有可能造成不同的數(shù)據(jù)庫不能兼容。當然Redis和NoSQL也無法限制。
當然,假如你現(xiàn)在就用Mybatis,而且數(shù)據(jù)庫使用的是Mysql,這方面就沒有太大影響了。
接下來說說優(yōu)點:
- 減少了接口數(shù)量及接口復雜度。原本針對不同的角色,可能會區(qū)分不同的接口或者在接口實現(xiàn)時利用流程控制邏輯來區(qū)分不同的條件。有了數(shù)據(jù)權限控制,代碼中只用寫基本邏輯,權限過濾由底層機制自動處理。
- 提高了數(shù)據(jù)權限控制的靈活性。例如原本只有主管能查本部門下組織架構/訂單數(shù)據(jù),現(xiàn)在新增助理角色,能夠查詢本部門下組織架構,不能查詢訂單。這樣的話普通的寫法就需要調(diào)整邏輯控制,使用數(shù)據(jù)權限控制的話,直接修改配置就好。
數(shù)據(jù)權限實現(xiàn)
上一節(jié)就提及了實現(xiàn)原理,是基于Mybatis的plugins)實現(xiàn)。
MyBatis 允許你在已映射語句執(zhí)行過程中的某一點進行攔截調(diào)用。默認情況下,MyBatis 允許使用插件來攔截的方法調(diào)用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
Mybatis的插件機制目前比較出名的實現(xiàn)應該就是PageHelper項目了,在做這個實現(xiàn)的時候也參考了PageHelper項目的實現(xiàn)方式。所以權限控制插件的類命名為PermissionHelper。
機制是依托于Mybatis的plugins機制,實際SQL處理的時候基于jsqlparser這個包。
設計中包含兩個類,一個是保存角色與權限的實體類命名為PermissionRule,一個是根據(jù)實體變更底層SQL語句的主體方法類PermissionHelper。
首先來看下PermissionRule的結構:
public class PermissionRule { private static final Log log = LogFactory.getLog(PermissionRule.class); /** * codeName<br> * 適用角色列表<br> * 格式如: ,RoleA,RoleB, */ private String roles; /** * codeValue<br> * 主實體,多表聯(lián)合 * 格式如: ,SystemCode,User, */ private String fromEntity; /** * codeDesc<br> * 過濾表達式字段, <br> * <code>{uid}</code>會自動替換為當前用戶的userId<br> * <code>{me}</code> main entity 主實體名稱 * <code>{me.a}</code> main entity alias 主實體別名 * 格式如: * <ul> * <li>userId = {uid}</li> * <li>(userId = {uid} AND authType > 3)</li> * <li>((userId = {uid} AND authType) > 3 OR (dept in (select dept from depts where manager.id = {uid})))</li> * </ul> */ private String exps; /** * codeShowName<br> * 規(guī)則說明 */ private String ruleComment; }
看完這個結構,基本能夠理解設計的思路了。數(shù)據(jù)結構中保存如下幾個字段:
- 角色列表:需要使用此規(guī)則的角色,可以多個,使用英文逗號隔開。
- 實體列表:對應的規(guī)則應用的實體(這里指的是表結構中的表名,可能你的實體是駝峰而數(shù)據(jù)庫是蛇形,所以這里要放蛇形那個),可以多個,使用英文逗號隔開。
- 表達式:表達式就是數(shù)據(jù)權限控制的核心了。簡單的說這里的表達式就是一段SQL語句,其中設置了一些可替換值,底層會用對應運行時的變量替換對應內(nèi)容,從而達到增加條件的效果。
- 規(guī)則說明:單純的一個說明字段。
核心流程
系統(tǒng)啟動時,首先從數(shù)據(jù)庫加載出所有的規(guī)則。底層利用插件機制來攔截所有的查詢語句,進入查詢攔截方法后,首先根據(jù)當前用戶的權限列表篩選出PermissionRule列表,然后循環(huán)列表中的規(guī)則,對語句中符合實體列表的表進行條件增加,最終生成處理后的SQL語句,退出攔截器,Mybatis執(zhí)行處理后SQL并返回結果。
講完PermissionRule,再來看看PermissionHelper,首先是頭:
@Intercepts({@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})}) public class PermissionHelper implements Interceptor { }
頭部只是標準的Mybatis攔截器寫法,注解中的Signature決定了你的代碼對哪些方法攔截,update實際上針對修改(Update)、刪除(Delete)生效,query是對查詢(Select)生效。
下面給出針對Select注入查詢條件限制的完整代碼:
private String processSelectSql(String sql, List<PermissionRule> rules, UserDefaultZimpl principal) { try { String replaceSql = null; Select select = (Select) CCJSqlParserUtil.parse(sql); PlainSelect selectBody = (PlainSelect) select.getSelectBody(); String mainTable = null; if (selectBody.getFromItem() instanceof Table) { mainTable = ((Table) selectBody.getFromItem()).getName().replace("`", ""); } else if (selectBody.getFromItem() instanceof SubSelect) { replaceSql = processSelectSql(((SubSelect) selectBody.getFromItem()).getSelectBody().toString(), rules, principal); } if (!ValidUtil.isEmpty(replaceSql)) { sql = sql.replace(((SubSelect) selectBody.getFromItem()).getSelectBody().toString(), replaceSql); } String mainTableAlias = mainTable; try { mainTableAlias = selectBody.getFromItem().getAlias().getName(); } catch (Exception e) { log.debug("當前sql中, " + mainTable + " 沒有設置別名"); } String condExpr = null; PermissionRule realRuls = null; for (PermissionRule rule : rules) { for (Object roleStr : principal.getRoles()) { if (rule.getRoles().indexOf("," + roleStr + ",") != -1) { if (rule.getFromEntity().indexOf("," + mainTable + ",") != -1) { // 若主表匹配規(guī)則主體,則直接使用本規(guī)則 realRuls = rule; condExpr = rule.getExps().replace("{uid}", UserDefaultUtil.getUserId().toString()).replace("{bid}", UserDefaultUtil.getBusinessId().toString()).replace("{me}", mainTable).replace("{me.a}", mainTableAlias); if (selectBody.getWhere() == null) { selectBody.setWhere(CCJSqlParserUtil.parseCondExpression(condExpr)); } else { AndExpression and = new AndExpression(selectBody.getWhere(), CCJSqlParserUtil.parseCondExpression(condExpr)); selectBody.setWhere(and); } } try { String joinTable = null; String joinTableAlias = null; for (Join j : selectBody.getJoins()) { if (rule.getFromEntity().indexOf("," + ((Table) j.getRightItem()).getName() + ",") != -1) { // 當主表不能匹配時,匹配所有join,使用符合條件的第一個表的規(guī)則。 realRuls = rule; joinTable = ((Table) j.getRightItem()).getName(); joinTableAlias = j.getRightItem().getAlias().getName(); condExpr = rule.getExps().replace("{uid}", UserDefaultUtil.getUserId().toString()).replace("{bid}", UserDefaultUtil.getBusinessId().toString()).replace("{me}", joinTable).replace("{me.a}", joinTableAlias); if (j.getOnExpression() == null) { j.setOnExpression(CCJSqlParserUtil.parseCondExpression(condExpr)); } else { AndExpression and = new AndExpression(j.getOnExpression(), CCJSqlParserUtil.parseCondExpression(condExpr)); j.setOnExpression(and); } } } } catch (Exception e) { log.debug("當前sql沒有join的部分!"); } } } } if (realRuls == null) return sql; // 沒有合適規(guī)則直接退出。 if (sql.indexOf("limit ?,?") != -1 && select.toString().indexOf("LIMIT ? OFFSET ?") != -1) { sql = select.toString().replace("LIMIT ? OFFSET ?", "limit ?,?"); } else { sql = select.toString(); } } catch (JSQLParserException e) { log.error("change sql error .", e); } return sql; }
重點思路
重點其實就在于Sql的解析和條件注入,使用開源項目JSqlParser。
- 解析出MainTable和JoinTable。from之后跟著的稱為MainTable,join之后跟著的稱為JoinTable。這兩個就是我們PermissionRule需要匹配的表名,PermissionRule::fromEntity字段。
- 解析出MainTable的where和JoinTable的on后面的條件。使用and連接原本的條件和待注入的條件,PermissionRule::exps字段。
- 使用當前登錄的用戶信息(放在緩存中),替換條件表達式中的值。
- 某些情況需要忽略權限,可以考慮使用ThreadLocal(單機)/Redis(集群)來控制。
結束語
想要達到無感知的數(shù)據(jù)權限控制,只有機制控制這么一條路。本文選擇的是通過底層攔截Sql語句,并且針對對應表注入條件語句這么一種做法。應該是非常經(jīng)濟的做法,只是基于文本處理,不會給系統(tǒng)帶來太大的負擔,而且能夠達到理想中的效果。大家也可以提出其他的見解和思路。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
- mybatis-plus數(shù)據(jù)權限實現(xiàn)代碼
- MyBatis-Plus攔截器實現(xiàn)數(shù)據(jù)權限控制的示例
- Mybatis-plus數(shù)據(jù)權限DataPermissionInterceptor實現(xiàn)
- Mybatis攔截器實現(xiàn)數(shù)據(jù)權限的示例代碼
- mybatis-plus團隊新作mybatis-mate實現(xiàn)數(shù)據(jù)權限
- Springboot+mybatis-plus+注解實現(xiàn)數(shù)據(jù)權限隔離
- mybatis攔截器實現(xiàn)數(shù)據(jù)權限項目實踐
相關文章
老生常談foreach(增強for循環(huán))和for的區(qū)別
下面小編就為大家?guī)硪黄仙U刦oreach(增強for循環(huán))和for的區(qū)別。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09Java8新特性之Base64詳解_動力節(jié)點Java學院整理
這篇文章主要為大家詳細介紹了Java8新特性之Base64的相關資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06Spring Boot優(yōu)化后啟動速度快到飛起技巧示例
這篇文章主要為大家介紹了Spring Boot優(yōu)化后啟動速度快到飛起的技巧示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07java微信公眾號開發(fā)(搭建本地測試環(huán)境)
這篇文章主要介紹了java微信公眾號開發(fā),主要內(nèi)容有測試公眾號與本地測試環(huán)境搭建,需要的朋友可以參考下2015-12-12