權限管理模塊
前言
如果你是爲自己公司開發系統的話,剛開始很有可能並沒有涉及到複雜權限控制,或者當時的開發週期短或者老闆拍板決定的不使用或直接用一個int值來控制角色,但隨着項目的擴展,會發現權限管理起來越來越麻煩,這個時候你可以考慮直接使用現成的權限管理框架或者自己整一個。而我,選擇了後者。
資源管理
設計思路及可能遇到的問題
時間還是有點久了,就簡單提下當時的思路。
資源訪問路徑
我們可以把所有的請求都當成一個資源,包括頁面的加載或者業務層的某一個判斷等等。那麼資源的訪問路徑就可以是如下順序:
資源分類
每一個權限就相當於一個資源,資源自然是要進行分來,這樣不管是查詢還是使用都會方便很多。分類方法的話可以根據資源的使用情景進行分類,我這裏就分成5個類:
/**
* 接口類請求地址
*/
public static final int TYPE_URL = 1;
/**
* 資源展示類視圖
*/
public static final int TYPE_VIEW = 2;
/**
* 無頁面菜單
*/
public static final int TYPE_MENU = 3;
/**
* 頁面菜單
*/
public static final int TYPE_PAGE = 4;
/**
* 操作類:包含後臺接口和前端展示
*/
public static final int TYPE_ACTION = 5;
如果不是有很嚴謹的要求的話可以把5,2
合在一起,我這裏的建議是分開,雖然在代碼上沒有多大意義,但是有助於主觀區分,畢竟資源始終是要有人去維護的。至於1,3,4
主要是用於菜單加載,接口是不需要展示出來的,畢竟有些接口是不能暴露出去或者涉及的業務比較重要。至於菜單的和子菜單(也就是頁面)的區分也是用於主觀上的,畢竟代碼方面還是好處理的。
資源結構設計
既然資源分類已經出來了,那麼就可以根據分類來設計對象結構。
private Integer id;
private String resname;
private String key;
private String value;
private String path;
/**
* 圖標
*/
private String ico;
/**
* 從屬上級資源,菜單或者頁面
*/
private int flow;
/**
* 從屬上級的名稱
*/
private String flowname;
private String flowtree;
private int type;
private boolean ipcheck;
private boolean rolecheck;
private boolean logincheck;
id,resname,path,ico
沒什麼好說的,要說的話就是圖標可以選擇存前端樣式名稱比如:fa-sign-out
這裏重點說下其他幾個字段。首先我們要考慮的問題是如何把這些資源給串起來,這樣便於我們識別,畢竟同名資源是無法避免的。我這裏使用的辦法就是把這個資源的家族樹保存下來flowtree
,這裏的家族樹也只是保存了所有父級加上他自己的id,這樣在生成菜單或者資源樹或者對資源進行操作的時候就會方便很多。比如要查詢所有子菜單,就可以直接根據flowtree來模糊匹配就行。但是需要注意的是,這個id不能從一個小數開始,這樣會把其他的也給查出來,比如:查詢5的子節點,但是會把15,25等等也給查出來,他們可能並不是5的子節點
字段flow
的用處主要是用於把直接父級資源的家族樹給拉去出來,這樣就可以直接設置當前資源的家族樹了,至於怎麼獲取自己的ID,如果是存入數據庫的話,可以直接查詢出來,比如MySql裏就是:
SELECT LAST_INSERT_ID();
至於ipcheck,relecheck,logincheck
則是用於條件檢查的,分別對應 白名單,角色,登錄
資源快速檢索
然後重點來了,當把所有資源都加載進內存後,資源的檢索就決定了權限管理模塊的好壞,如果一個資源檢索很慢,那和直接用角色ID做判斷有何區別,而且也會也行業務效率,甚至影響服務器性能。
爲了達到更快的檢索我把資源添加了不同的索引,這裏的索引指的是在內存中建立索引來達到快速查詢的目的,我使用的是 HashMap
映射表,畢竟哈希表的查詢速度公認的。
那麼如何建立索引就比較重要了,在這個模塊裏我一共建立了4個索引,雖然有個索引一直沒用到就是了
//PATH索引
private static Map<String,Resources> pathMap = new HashMap<String, Resources>();
//KeyValue索引
private static Map<String,Map<String,Resources>> kvMap = new HashMap<String, Map<String,Resources>>();
//ResourcesID索引
private static Map<Integer,Resources> resourcesMap = new HashMap<Integer,Resources>();
//RoleIDResourcesTree索引
private static Map<Integer,ResourcesTree> roleResourcesTree = new HashMap<Integer, ResourcesTree>();
第一個:地址索引 - pathMap
這個索引主要是用於權限過濾器使用,根據保存的資源地址(path)
來直接獲取資源,畢竟請求地址是很好獲取的,甚至可以用模糊匹配的方式來批量授權,這個時候對第三方模塊就很實用了,畢竟不可能去把它所有的請求都翻出來添加進去。比如我們可以給druid
連接池自帶的監控進行授權
第二個:KeyValue索引 - kvMap
首先說下之前沒有提到的 key,value
這兩個字段,key
這個字段由資源所屬的菜單.子菜單
組成,如果是多級菜單可以直接拼接上去,方便前端菜單樹的頁面定位,value
則是用來區分不同的資源用。比如:權限管理->資源管理
頁面 key
設置成 rbac.resources
,value
設置成resources
,資源編輯權限操作
的 key
和資源管理
頁面一樣,然後value
設置成edit
根據key,value
就可以生成一個多級的HasmMap
映射從而達到快速檢索的目的。
第三個:資源ID索引 - resourcesMap
這個就只是單純的把資源ID提出來做的映射,實際使用中發現並沒有用到
第四個:角色菜單樹索引 - roleResourcesTree
這裏要首先說下菜單樹的定義:
private Integer id;
private Resources resources;
private Map<Integer,ResourcesTree> children;
這裏的ID
指的是資源ID
,resources
則是當前的資源,children
則是該資源下的子資源。
然後第一層 Map
的key
就是角色的ID,通過這個映射就可以直接在前端生成角色對應的菜單。
權限檢查
在拿到資源後就可以進行權限檢查了,根據ipcheck,rolecheck,logincheck
這幾個字段做相應的檢查。
ipcheck
白名單的話直接根據設置的白名單列表進行匹配即可。logincheck
則直接進行判斷當前請求用戶是否已經登錄即可。rolecheck
則是根據獲取到的資源ID對用戶進行檢查。
部分代碼參考
資源索引生成
logger.info("生成資源索引");
for(;!resList.isEmpty();){
Resources resItem = resList.remove(0);
if(StringUtils.isNotBlank(resItem.getPath())){
pathMapTemp.put(resItem.getPath(), resItem);
}
if(kvMapTemp.containsKey(resItem.getKey())){
kvMapTemp.get(resItem.getKey()).put(resItem.getValue(), resItem);
}else{
Map<String,Resources> vMap = new HashMap<String, Resources>();
kvMapTemp.put(resItem.getKey(), vMap);
vMap.put(resItem.getValue(), resItem);
}
resourcesMapTemp.put(resItem.getId(), resItem);
}
logger.info("生成資源索引完成");
logger.info("生成角色主菜單索引");
Map<Integer,ResourcesTree> roleResourcesTreeTemp = new HashMap<Integer, ResourcesTree>();
for(Integer roleId:roleMapTemp.keySet()){
Role roleItem = roleMapTemp.get(roleId);
ResourcesTree rootTree = new ResourcesTree();//根目錄
Set<Integer> created = new HashSet<Integer>();//已經生成過多的資源ID
List<String> resListItem = new ArrayList<String>(roleItem.Reslist);//不能直接做移除,要複製一個副本來操作
for(;!resListItem.isEmpty();){
String resId = resListItem.remove(0);
Resources resItem = resourcesMapTemp.get(Integer.parseInt(resId));
if(resItem.getType()==Resources.TYPE_MENU || resItem.getType()==Resources.TYPE_PAGE){
//是否存在父級
if(resItem.getFlow()!=0){
//檢測父級是否創建
if(created.contains(resItem.getFlow())){
String flowtree = resItem.getFlowtree().replaceAll("\\,?\\d+$", "");//剔除自己
ResourcesTree root = rootTree;
//根據家族樹查找父級
for(String key:flowtree.split(",")){
root = root.getChildren().get(Integer.parseInt(key));
}
//創建子節點
ResourcesTree treeItem = new ResourcesTree();
treeItem.setId(resItem.getId());
treeItem.setResources(resItem);
if(root.getChildren()==null){
Map<Integer,ResourcesTree> children = new LinkedHashMap<Integer, ResourcesTree>();
children.put(resItem.getId(), treeItem);
root.setChildren(children);
}else{
Map<Integer,ResourcesTree> children = root.getChildren();
children.put(resItem.getId(), treeItem);
}
created.add(resItem.getId());
}else{
//判斷父節點是否被授權,否這跳過創建,並記錄已存日誌
if(!resListItem.contains(resItem.getFlow())){
logger.error("權限異常["+resId+"]:"+roleItem.getId()+"/"+roleItem.getRolename());
System.out.println("權限異常["+resId+"]:"+roleItem.getId()+"/"+roleItem.getRolename());
break;
}else{
resListItem.add(resId);
//父級未創建放回
continue;
}
}
}else{
ResourcesTree treeItem = new ResourcesTree();
treeItem.setId(resItem.getId());
treeItem.setResources(resItem);
if(rootTree.getChildren()==null){
Map<Integer,ResourcesTree> children = new LinkedHashMap<Integer, ResourcesTree>();
children.put(resItem.getId(), treeItem);
rootTree.setChildren(children);
}else{
Map<Integer,ResourcesTree> children = rootTree.getChildren();
children.put(resItem.getId(), treeItem);
}
created.add(resItem.getId());
}
}
}
roleResourcesTreeTemp.put(roleId, rootTree);
}
logger.info("生成角色主菜單索引完成");
索引查詢接口
/**
* 獲取菜單樹
* @return
*/
public static ResourcesTree getTreeByRoleId(Integer roleId){
Role role = getRoleByID(roleId);
if(role == null || !roleResourcesTree.containsKey(roleId)){
return new ResourcesTree();
}
return roleResourcesTree.get(roleId);
}
/**
* 根據KeyValue獲取資源
* @return
*/
public static Resources getResourcesByKeyValue(String key,String value){
if(kvMap.containsKey(key)){
return kvMap.get(key).get(value);
}
return null;
}
/**
* 根據path獲取資源
*/
public static Resources getResourcesByPath(String path){
return pathMap.get(path);
}
/**
* 獲取角色
*/
public static Role getRoleByID(Integer roleId){
return roleMap.get(roleId);
}
public static boolean hasResourcesSet(String key,String value,Integer roleId){
Resources res = getResourcesByKeyValue(key, value);
if(res!=null){
Role role = getRoleByID(roleId);
if(role!=null){
return role.Reslist.contains(res.getId().toString());
}
}
return false;
}
public static boolean hasResourcesSet(String path,Integer roleId){
Resources res = getResourcesByPath(path);
if(res!=null){
Role role = getRoleByID(roleId);
if(role!=null){
return role.Reslist.contains(res.getId().toString());
}
}
return false;
}
角色管理
角色管理就比較靈活了,我這裏也是做了一個角色ID索引
用作用戶快速獲取自己的角色。
這裏就直接貼出角色和用戶的設計:
public class Role {
private int id;
private String rolename;
/**
* 父級
*/
private int parent;
private String pname;
/**
* 家族樹
*/
private String family;
private String info;
private boolean admin;
public List<String> Reslist = new ArrayList<String>();
public List<String> FamilyList = new ArrayList<String>();
public void setFamily(String family){
List<String> familyTemp = new ArrayList<String>();
if(StringUtils.isNotBlank(family)){
for(String item:family.split(",")){
familyTemp.add(item.trim());
}
}
FamilyList = familyTemp;
this.family = family;
}
public void setResList(String resList){
List<String> listTemp = new ArrayList<String>();
if(StringUtils.isNotBlank(resList)){
for(String item:resList.split(",")){
listTemp.add(item.trim());
}
}
Reslist = listTemp;
}
}
public class UserObject {
protected Integer uid;
protected Integer role;
protected String roleName;
public boolean hasItem(String key,String value){
return ManagerUtil.hasResourcesSet(key, value, role);
}
}
角色的家族樹就可以靈活一點,這樣方便查詢子角色,比如員工管理裏面,人事和財務的都要查看其他人的部分信息等等,然後 hasItem
則是用來檢查角色是否擁有該資源的接口,主要用於前端
和業務層
的需求,至於源碼的話後面整理出來會提交到GitHub