一個SSM平臺,當初設計的時候沒有想着做數據權限,以爲光是按鈕級權限就足夠,但是後期隨着業務的擴展,其他第三方公司需要使用我們的數據後臺,而且可能公司有多家,每家還有多個部門,崗位之類的。
之前的按鈕級權限採用通用設計,五張表,用戶表--用戶角色中間表--角色表--角色資源中間表--資源表。
ER圖標如下:
數據權限對上面的幾張表改動不大,多了這麼幾張表。應用數據模塊表--數據模塊用戶數據表(2個外鍵,1個任意鍵(沒有強制外鍵,但是是多張表的id列)) ,組織表。並且在用戶表中添加組織表外鍵,在角色表中添加數據訪問級別字段(0-個人 1-組織 2-所有)。
ER圖如下:
這套數據權限的重點是查詢時的邏輯,當然這套邏輯也不算很完善(組織架構不完善),但是希望給自己以後一些啓發,能設計寫更好的邏輯,也希望看到的人能有一些思路。
首先是組織表,這個表就是存公司,公司部門,公司崗位都在一張表,具有層級關係,但是隻有簡單的3個字段id,name,pId。
然後是應用數據模塊表,這張表是記錄那些模塊(或者那張表)的數據需要進入權限管理,因爲有些模塊可能只需要按鈕級權限就足夠,只有某些設計業務的,跟第三方公司相關的數據才需要被數據權限管理,所有這張表就是記錄這些需要進入數據權限的模塊或表。字段如下圖:
name只是一個顯示的名稱,不進入整套邏輯,tableName我設計出來之後也沒有進入整個邏輯,目前也是沒用的。主要就是id和domain字段,domain就是普通的JavaBean,有getter/setter那種。
然後第三張表是中間表:如下圖
adminId就是用戶,appdmId是模塊表id,objId不只是某一張表的外鍵,是任何表的外鍵,不知道你有沒有過附件表的設計,就是把整個項目的圖片,文件等放到一張表裏去統一管理,思路類似,我貼一下我的附件表設計,如下圖:
瞭解清楚上傳表設計之後,接下來先說明在第三方公司賬號登錄平臺後訪問被管理(就是訪問模塊表中的數據)的數據時的邏輯思路。
當訪問列表方法時,會進入一個統一的selectAll方法,這個方法我簡單說明下:因爲整個數據庫的70+張表都是我設計的,所以所有表的設計習慣都是我一個人的習慣,包括dao,service,xml,Controller,jsp等都是我用自己寫的工具生成的,所以很多命名,方法名等都是統一的,不會有意外。而這裏的selectAll方法是獲取整張表的數據(利用的緩存,只訪問了一次數據庫,後面都是從緩存中取數據),並且分頁是用的前端(數據管理平臺端)分頁,後臺只有接口是後臺分頁。
回到上一個邏輯,訪問列表時,會訪問selectAll,這個時候會進入緩存切面,同時我的數據權限也在這個切面完成,在切面中通過反射獲取是那個模型的selectAll方法被訪問,從而得到這個模型的全限定名。這裏最嚴謹的是通過sql拿到表,但是因爲底層是mybatis,我在切面這裏拿不到被執行的sql,而且後面是通過緩存也拿不到sql,所有才選擇模型,也是這個原因模塊表中的tableName字段才無效。
拿到模型的全限定名之後查詢數據庫模塊表,看當前模型對應的模塊數據需不需要(查到數據就是需要權限管理,查不到就是不需要)權限管理,如果查不到就直接返回selectAll的結果,如果需要,進入下一步。
接下來,獲取當前登陸者(需要注入Session),拿到當前用戶的訪問級別(0-個人 1-組織 2-所有),如果是所有則直接返回selectAll的結果,不做任何處理,因爲selectAll本來查的就是全部。這裏不考慮任何緩存的問題,其實很多數據都是在緩存中拿,數據庫一般之訪問一次,需要等到有修改的時候,去清緩存,然後再查數據庫,可以看下我的另一篇設計緩存的文章。
那麼接下來就是兩個處理:0-個人 1-組織
對於0-個人的處理很簡單,有一張中間表(包含用戶id,模塊id,數據id),根據用戶和模塊查詢對應模塊的數據id出來,然後進行過濾就行,過濾我使用的一個map,如下圖:
最後有一個遍歷數據是因爲我的selectAll統一返回List,所以需要再轉一次,可以考慮更合適的設計,不多說。
第二個處理 1-組織
對於1-組織的處理,通過用戶,查詢組織,查詢這個組織的所有子組織(部門,崗位等),得到所有組織(不需要層級關係),查到這些組織下所有的用戶,再通過這些用戶去中間表查詢數據,重複上述的過濾處理,如下圖:
這樣處理之後,數據權限的過濾就做完了。
再來談談數據權限和數據的綁定。
因爲所有的數據都是通過insert方法添加到數據庫,那麼我對insert進行了切面。
再執行insert方法切面中邏輯如下:
通過反射獲取參數,參數都是模型(我個人的設計),得到模型後去模塊表中查,當前模型對應的模塊數據有沒有被管理,同樣是根據有沒有查到數據來判斷的,查到了模塊數據,則代表被數據權限管理了。
然後每個insert方法的參數執行完DML操作之後,都會封裝一個id進參數對象中,mybatis的useGeneratedKeys="true" keyProperty="id"這兩個屬性。那麼我能得到這條數據的id。加上之前得到的模塊id,添加到中間表,結束。
這樣就完成了數據的綁定。
最後,爲了以防以後會突然加一些模塊進行數據管理,做了一個頁面,手動對用戶和數據進行數據綁定的功能,這個功能我給出後臺邏輯代碼和頁面顯示相關代碼,其他不多做介紹:
package com.ssm.service.appdata; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; import com.ssm.domain.appdatamodel.AppDataModel; import com.ssm.service.appdatamodel.AppDataModelService; @SuppressWarnings("all") @Service public class AppDataService { @Autowired private AppDataModelService appDataModelService; @Transactional public void insertAppData(String ids, Long adminId) { //通過模型獲取應用模塊id的集合 Map<String,Long> map = new HashMap<>(); try { String[] allIds = ids.split(","); for (String idStr : allIds) { if (idStr.contains("pId_")) { //得到模塊id Long id = Long.parseLong(idStr.replace("pId_","")); AppDataModel adm = appDataModelService.selectOneById(id); String domain = adm.getDomain(); //模型-模塊id map.put(domain, id); //模塊id+用戶id刪應用數據 appDataModelService.deleteAdminAndModelAndObj(adminId,id); }else { String[] doamin_id = idStr.split("_"); String domain = doamin_id[0]; Long id = Long.parseLong(doamin_id[1]); Long admId = map.get(domain); if (admId!=null&&adminId>0) { //模塊id+用戶id+應用id 添加 Map<String,Long> m = new HashMap<>(); m.put("adminId", adminId); m.put("appdmId", admId); m.put("objId", id); appDataModelService.insertAdminAndObjIdAndModel(m); } } } } catch (Exception e) { throw new RuntimeException(e.getMessage()); } } public List<AppData> getAllDate() { List<AppDataModel> list = appDataModelService.selectAll(); List<AppData> ads = new ArrayList<>(); for (AppDataModel adm : list) { Long id = adm.getId(); String text = adm.getName(); AppData appData = new AppData(); appData.setId("pId_"+id); appData.setText(text); appData.setpId("0"); String domain = adm.getDomain(); int index = domain.lastIndexOf("."); String className = domain.substring(index+1); WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext(); //獲取dao對象 Object dao = context.getBean(className.toLowerCase()+"DAO"); try { Method method = dao.getClass().getMethod("selectAll"); List<?> objs = (List)method.invoke(dao); List<AppData> children = new ArrayList<>(); for (Object obj : objs) { AppData ad = new AppData(); ad.setId(domain+"_"+obj.getClass().getMethod("getId").invoke(obj)); ad.setText(obj.toString()); ad.setpId("pId_"+id); children.add(ad); } appData.setChildren(children); } catch (Exception e) { e.printStackTrace(); } ads.add(appData); } return ads; } public List<String> getAppDataByAdminId(Long adminId) { List<AppDataModel> list = appDataModelService.selectAll(); List<String> ads = new ArrayList<>(); for (AppDataModel adm : list) { Map<String, Long> map = new HashMap<>(); map.put("adminId", adminId); map.put("appdmId", adm.getId()); String domain = adm.getDomain(); //查到當前模塊下有的數據 List<Long> ids = appDataModelService.selectAllByAdminIdAndModel(map); for (Long id : ids) { ads.add(domain+"_"+id); } } return ads; } }
有一個數據封裝的類,這個類是爲了頁面上跟ZTree綁定:如下:
package com.ssm.service.appdata; import java.util.ArrayList; import java.util.List; public class AppData { private String id; private String text; private String pId; private List<AppData> children = new ArrayList<>(); public String getpId() { return pId; } public void setpId(String pId) { this.pId = pId; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getText() { return text; } public void setText(String text) { this.text = text; } public List<AppData> getChildren() { return children; } public void setChildren(List<AppData> children) { this.children = children; } }
頁面代碼如下:
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <%@ taglib tagdir="/WEB-INF/tags" prefix="villa"%> <% String path = request.getContextPath(); System.out.println(path); pageContext.setAttribute("path", path); %> <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <meta name="renderer" content="webkit|ie-comp|ie-stand"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" /> <meta http-equiv="Cache-Control" content="no-siteapp" /> <title>添加</title> <jsp:include page="/commons/jsp/common_css.jsp"></jsp:include> <jsp:include page="/commons/jsp/common_js.jsp"></jsp:include> <link rel="stylesheet" href="${path}/H-ui.admin/lib/zTree/v3/css/metroStyle/metroStyle.css" type="text/css"> </head> <body> <article class="page-container"> <form class="form form-horizontal" id="form"> <div class="row cl"> <label class="form-label col-xs-1 text-l"><span class="c-red">*</span>用戶:</label> <div class="formControls col-xs-11"> <select class="select" name="adminId" size="1" style="height: 30px;" onchange="showResources(this.value)"> <c:forEach items="${list}" var="obj"> <option value="${obj.id}">${obj.name}</option> </c:forEach> </select> </div> </div> <div class="panel panel-secondary mt-20" style="overflow: auto;"> <div class="panel-header"> 應用數據 </div> <div class="panel-body"> <div class="bg left"> <ul id="treeDemo" class="ztree"></ul> </div> </div> </div> <div class="row cl"> <div class="col-xs-8 col-xs-offset-3"> <input class="btn btn-primary radius" type="submit" value=" 提交 "> </div> </div> </form> </article> <!--請在下方寫此頁面業務相關的腳本--> <script type="text/javascript" src="${path}/H-ui.admin/lib/My97DatePicker/4.8/WdatePicker.js"></script> <script type="text/javascript" src="${path}/H-ui.admin/lib/datatables/1.10.0/jquery.dataTables.min.js"></script> <script type="text/javascript" src="${path}/H-ui.admin/lib/zTree/v3/js/jquery.ztree.core-3.5.min.js"></script> <script type="text/javascript" src="${path}/H-ui.admin/lib/zTree/v3/js/jquery.ztree.excheck-3.5.min.js"></script> <script type="text/javascript" src="${path}/H-ui.admin/lib/zTree/v3/js/jquery.ztree.exhide-3.5.min.js"></script> <script type="text/javascript"> var zTree; $(function(){ var setting = { view: { selectedMulti: false }, check: { enable: true ,chkStyle: 'checkbox' }, data: { simpleData: { enable: true } }, edit: { enable: true } }; var zNodes =[]; $.post("${path}/admin/bindData.data",function(data){ data = JSON.parse(data); for(var i = 0;i<data.length;i++){ var obj = data[i]; zNodes.push({ id:obj.id, pId:obj.pId, name:obj.text, key:obj.id, open:false }); for(var j = 0;j<obj.children.length;j++){ var child = obj.children[j]; zNodes.push({ id:child.id, pId:child.pId, name:child.text, key:child.id, }); } } zTree = $.fn.zTree.init($("#treeDemo"), setting, zNodes); // 獲取select中的用戶ID var adminId = $("select[name=adminId]").val(); // 回顯 showResources(adminId); }) $("#form").validate({ rules:{ }, onkeyup:false, focusCleanup:true, success:"valid", submitHandler:function(form){ //獲取被勾選的節點的資源id var nodes = zTree.getCheckedNodes(true); var ids = ""; for (var i = 0; i < nodes.length; i++) { ids+=nodes[i].id+","; } $(form).ajaxSubmit({ type: 'post', url: "${path}/admin/bindData" , dataType:'json', data:{ ids:ids }, success: function(data){ if (data.result) { layer.msg(data.msg,{icon:1,time:1000},function(){ var index = parent.layer.getFrameIndex(window.name); parent.$('.btn-refresh').click(); parent.layer.close(index); }); }else{ layer.msg(data.msg,{icon:2,time:1000}); } }, error: function(XmlHttpRequest, textStatus, errorThrown){ layer.msg('網絡異常,請稍後重試!',{icon:2,time:1000}); } }); } }); }); function showResources(adminId){ console.log(adminId); zTree.checkAllNodes(false); $.post("${path}/admin/getAppDataByAdminId",{adminId:adminId},function(res){ res = JSON.parse(res); for (var i = 0; i < res.length; i++) { var id = res[i]; var nodes = zTree.getNodesByParam("key",id); if(nodes.length>0){ zTree.checkNode(nodes[0],true,false); } } }); } </script> </body> </html>
如果有好的通用設計,希望不吝賜教。我這麼做只是希望將數據權限跟業務數據分開。