問題引出
最近,許多學員反饋項目中需要處理數據權限,但是不知道怎么處理比較合適。這篇手記將針對這個問題,給出一種比較通用且容易擴展的數據權限設計方案。
現狀
目前流行的權限框架已經有支持數據權限的了,但是需要配置在接口和方法上,擴展性不是很好,那么怎樣做能讓擴展性最大化呢?
很容易想到的就是:將數據權限的控制放到數據庫里存儲,在權限攔截時先判斷接口是否有權訪問,在接口有權訪問后,接下來根據配置的條件判斷是否有權使用指定的參數值。(做的更高級些,可以對返回的結果進行檢查,包含了某個值的某個對象不允許訪問的話,也當做無權訪問處理,這篇手記里暫時不考慮這個情況)。具體怎么做呢?
數據庫設計
先從數據庫表設計說起,首先定義一個數據權限控制表結構:
CREATE TABLE `sys_acl_data` (
`id` int(11) NOT NULL,
`acl_id` int(11) NOT NULL COMMENT '對應權限表主鍵',
`status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '狀態,1:可用,0:不可用',
`param` varchar(20) NOT NULL DEFAULT '' COMMENT '參數',
`operation` int(11) NOT NULL DEFAULT 0 COMMENT '操作類型,0;等於,1:大於,2:小於,3:大於等於,4:小於等於,5:包含,6:介於之間,。。。',
`value1` varchar(100) NOT NULL DEFAULT '0',
`value2` varchar(100) NOT NULL DEFAULT '0',
`next_param_op` int(11) NOT NULL DEFAULT 0 COMMENT '后續有參數時連接的關系,0:沒有其他參數控制,1:與&&,2:或||',
`seq` tinyint(4) NOT NULL DEFAULT '0' COMMENT '順序',
PRIMARY KEY (`id`),
INDEX `idx_acl_id` USING BTREE (`acl_id`)
) ENGINE=`InnoDB` COMMENT '數據權限表';
具體介紹一下每個字段含義:
主鍵 id;
acl_id 映射權限點表主鍵,代表每行記錄是針對哪個權限點的;
status 代表當前這條配置是否有效,方便臨時激活與禁用;
param 代表需要校驗的參數名,允許一個請求有多個參數參與數據校驗;如果參數復雜,比如包含對象,定義的參數可能為a.b.c 這種多級的形式,建議不要太復雜
operation 代表數據攔截的規則,使用數字代表是等於、大於、小於、大於等於、小於等於、包含、介於之間等,可以根據自己需要增加或減少支持的攔截規則
value1 和 value2 用來和param、operation組成一個關系表達式,比如:1<=a<2
next_param_op 字段根據需要使用,如果一個權限點支持多條數據規則時,連接兩個規則之間的操作,|| 還是 &&
seq 字段用於某個權限點包含多條數據權限規則時的順序

假設有這么一條數據,那么他的含義是:id為1(acl_id)的權限點,配置了一條有效(status=1)的數據規則,規則是:傳入參數id(param)的值要大於(operation)10(value1)
數據權限校驗邏輯
有了表結構后,接下來就是增加接口能對數據進行更新和獲取了,然后有個頁面能對其進行展示和新增操作了,這里就不占更多篇幅了,重點說一下邏輯的處理。
權限課程里原生實現一套權限管理部分已經對權限點做的基本管理和權限攔截就不在這里重復說了,具體看視頻和代碼就可以了,這里重點說一下如何在已有的權限上進行數據權限的擴展。首先給出url攔截核心代碼和權限校驗的核心代碼(單獨看這段代碼不去看課程的細節應該也能看懂個大概):
自定義filter攔截url判斷權限核心代碼:
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String servletPath = request.getServletPath();
Map requestMap = request.getParameterMap();
if (exclusionUrlSet.contains(servletPath)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
SysUser sysUser = RequestHolder.getCurrentUser();
if (sysUser == null) {
log.warn("someone visit {}, but no login, parameter:{}", servletPath, JsonMapper.obj2String(requestMap));
noAuth(request, response);
return;
}
SysCoreService sysCoreService = ApplicationContextHelper.popBean(SysCoreService.class);
if (!sysCoreService.hasUrlAcl(servletPath)) {
log.warn("{} visit {}, but no login, parameter:{}", JsonMapper.obj2String(sysUser), servletPath, JsonMapper.obj2String(requestMap));
noAuth(request, response);
return;
}
filterChain.doFilter(servletRequest, servletResponse);
return;
}
實際判斷一個url是否可訪問的核心代碼:
public boolean hasUrlAcl(String url) {
if (isSuperAdmin()) { // 超級管理員直接允許訪問
return true;
}
List<SysAcl> aclList = sysAclMapper.getByUrl(url); // 取出符合條件的權限點
if (CollectionUtils.isEmpty(aclList)) {
return true;
}
List<SysAcl> userAclList = getCurrentUserAclListFromCache();
Set<Integer> userAclIdSet = userAclList.stream().map(acl -> acl.getId()).collect(Collectors.toSet());
boolean hasValidAcl = false;
// 規則:只要有一個權限點有權限,那么我們就認為有訪問權限
for (SysAcl acl : aclList) { // ----------------------------------------------------- ①
// 判斷一個用戶是否具有某個權限點的訪問權限
if (acl == null || acl.getStatus() != 1) { // 權限點無效
continue;
}
hasValidAcl = true;
if (userAclIdSet.contains(acl.getId())) {
return true; // ------------------------------------------------------ ②
}
}
if (!hasValidAcl) {
return true; // ------------------------------------------------------- ③
}
return false;
}</code></pre></div><p>從代碼的 ① 處,可以拿到實際要判斷的權限點。在判斷某個指定的權限點已經有權限訪問時,代碼的 ②、③處,需要加入數據權限的校驗。</p><p>既然要校驗參數了,那么就需要把參數傳入 hasUrlAcl 這個方法。doFilter方法里的 Map requestMap = request.getParameterMap(); 的requestMap就是url的參數列表,這種方式對於某些特殊的post提交不是完全適用,比如通過body里傳遞json格式的參數。實際項目中怎么把參數傳遞到方法里,可以根據項目接口的實際定義來處理。</p><p>當hasUrlAcl拿到參數且判斷指定的權限點有權訪問時,去sys_acl_data表根據acl_id查詢出有效的規則列表,逐條判斷,這里注意許多細節的處理。1、單條規則的解讀,2、多條規則間的邏輯與和或,3、參數帶層級時的解讀(a.b.c這種),實際中可以根據項目中接口的定義規范來決定處理的復雜度。這個實現后,當url有權訪問時,沒有數據規則或者數據規則校驗通過時,這個url才算真正的有權訪問。</p><p>這時,肯定有人會問,我的接口是這樣定義的/a/{id}.json 這種的該如何做數據權限攔截呢?其實這種方式的接口,課程里目前稍微調整一下也可以支持,調整如下:</p><div class="highlight"><pre><code class="language-text"> SysAclMapper.xml:
<select id="getByUrl" parameterType="string" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM sys_acl
WHERE url = #{url} <!-- url is not null and url != '' and #{url} REGEXP url-->
</select></code></pre></div><p>注釋的內容是開啟正則匹配的,就是通過正則去匹配url,這里使用 url is not null and url != '' and #{url} REGEXP url 代替 url = #{url} ,然后在配置每個權限點時使用正則去配置每個權限點的url就可以了,比如剛才url配置權限校驗時可以配置成/a/[5<i>|6</i>].json。當然這種方式對權限管理員的正則表達式有一定的要求。這時,在取符合條件的url時校驗不過的權限點就取不出來了。取不出來不能直接就當做有處理,可以考慮遇到這種的再配置一個通配(/a/*.json)的權限,每次匹配到這種通配的url時必須保證匹配一個包含正則的校驗才算有權限就可以了。這個的細節可以自己做一些不同的處理,這里只提供一個大概的思路。</p><p><b>結尾</b></p><p>關於數據權限就先說到這里,個人認為上面的方式在擴展性方面會相對好一些,實現起來難度也不是很大,如有問題歡迎指出。</p><p>其他權限手記</p><p><a href="https://link.zhihu.com/?target=http%3A//www.imooc.com/article/20741" class=" wrap external" target="_blank" rel="nofollow noreferrer" data-za-detail-view-id="1043">改造電商交易后台權限管理過程</a></p><p><a href="https://link.zhihu.com/?target=http%3A//www.imooc.com/article/20763" class=" wrap external" target="_blank" rel="nofollow noreferrer" data-za-detail-view-id="1043">自定義JSP標簽自動完成對頁面按鈕做權限攔截處理</a></p><p class="ztext-empty-paragraph"><br></p><p>作者: _Jimin_ </p><p>鏈接:<a href="https://link.zhihu.com/?target=http%3A//www.imooc.com/article/21376" class=" external" target="_blank" rel="nofollow noreferrer" data-za-detail-view-id="1043"><span class="invisible">http://www.</span><span class="visible">imooc.com/article/21376</span><span class="invisible"></span></a></p><p>來源:慕課網</p><p>本文原創發布於慕課網 ,轉載請注明出處,謝謝合作!</p></div>