權限控制主要分爲兩塊,認證(Authentication)與受權(Authorization)。認證以後確認了身份正確,業務系統就會進行受權,如今業界比較流行的模型就是RBAC(Role-Based Access Control)。RBAC包含爲下面四個要素:用戶、角色、權限、資源。用戶是源頭,資源是目標,用戶綁定至角色,資源與權限關聯,最終將角色與權限關聯,就造成了比較完整靈活的權限控制模型。
資源是最終須要控制的標的物,可是咱們在一個業務系統中要將哪些元素做爲待控制的資源呢?我將系統中待控制的資源分爲三類:html
如今業內廣泛的實現方案實際上很粗放,就是單純的「菜單控制」,經過菜單顯示與否來達到控制權限的目的。
我仔細分析過,如今你們作的平臺分爲To C和To B兩種:前端
因此針對如今的狀況,考慮成本與產出,大部分設計者也不肯意在權限上進行太多的研發力量。
菜單和界面元素通常都是由前端編碼配合存儲數據實現,URL訪問資源的控制也有一些框架好比SpringSecurity,Shiro。
目前我尚未找到過數據權限控制的框架或者方法,因此本身整理了一份。java
數據權限控制最終的效果是會要求在同一個數據請求方法中,根據不一樣的權限返回不一樣的數據集,並且無需而且不能由研發編碼控制。這樣你們的第一想法應該就是AOP,攔截全部的底層方法,加入過濾條件。這樣的方式兼容性較強,可是複雜程度也會更高。咱們這套系統中,採用的是利用Mybatis的plugin機制,在底層SQL解析時替換增長過濾條件。
這樣一套控制機制存在很明顯的優缺點,首先缺點:git
固然,假如你如今就用Mybatis,並且數據庫使用的是Mysql,這方面就沒有太大影響了。github
接下來講說優勢:sql
上一節就說起了實現原理,是基於Mybatis的plugins(查看官方文檔)實現。數據庫
MyBatis 容許你在已映射語句執行過程當中的某一點進行攔截調用。默認狀況下,MyBatis 容許使用插件來攔截的方法調用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)緩存
Mybatis的插件機制目前比較出名的實現應該就是PageHelper項目了,在作這個實現的時候也參考了PageHelper項目的實現方式。因此權限控制插件的類命名爲PermissionHelper。
機制是依託於Mybatis的plugins機制,實際SQL處理的時候基於jsqlparser這個包。
設計中包含兩個類,一個是保存角色與權限的實體類命名爲PermissionRule,一個是根據實體變動底層SQL語句的主體方法類PermissionHelper。數據結構
首先來看下PermissionRule的結構:mybatis
public class PermissionRule { private static final Log log = LogFactory.getLog(PermissionRule.class); /** * codeName<br> * 適用角色列表<br> * 格式如: ,RoleA,RoleB, */ private String roles; /** * codeValue<br> * 主實體,多表聯合 * 格式如: ,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> * 規則說明 */ private String ruleComment; }
看完這個結構,基本可以理解設計的思路了。數據結構中保存以下幾個字段:
核心流程
系統啓動時,首先從數據庫加載出全部的規則。底層利用插件機制來攔截全部的查詢語句,進入查詢攔截方法後,首先根據當前用戶的權限列表篩選出PermissionRule列表,而後循環列表中的規則,對語句中符合實體列表的表進行條件增長,最終生成處理後的SQL語句,退出攔截器,Mybatis執行處理後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) { // 若主表匹配規則主體,則直接使用本規則 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,使用符合條件的第一個表的規則。 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; // 沒有合適規則直接退出。 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。
想要達到無感知的數據權限控制,只有機制控制這麼一條路。本文選擇的是經過底層攔截Sql語句,而且針對對應表注入條件語句這麼一種作法。應該是很是經濟的作法,只是基於文本處理,不會給系統帶來太大的負擔,並且可以達到理想中的效果。你們也能夠提出其餘的看法和思路。