SSM項目後期添加數據權限設計

    一個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="&nbsp;&nbsp;提交&nbsp;&nbsp;">
			</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>

如果有好的通用設計,希望不吝賜教。我這麼做只是希望將數據權限跟業務數據分開。