項目中使用數據庫表+視圖+存儲過程+緩存的方式實現用戶權限的控制。通過用戶表、角色表、權限表以及用戶角色表、角色權限表兩個中間表可以得到一個用戶對應的權限有哪些。創建一個視圖將這五個表連接起來,可以查詢出每個用戶對應的權限有哪些。Java層通過調用存儲過程,存儲過程再查詢該視圖,用戶權限可以傳遞到Java層。Java層將用戶權限緩存起來,可以提高效率。本文主要介紹:“涉及到的數據庫表”、“創建連接表的視圖”、“調用視圖的存儲過程”、“Java層加載權限信息”、“Java層判斷用戶是否有權限”、“可以使用Excel管理角色權限信息”。
1、涉及到的數據庫表
涉及5個數據庫表:
(1)用戶表t_staff
DROP TABLE IF EXISTS T_Staff;
CREATE TABLE T_Staff -- 門店用戶
(
F_ID INT NOT NULL AUTO_INCREMENT, -- ID
F_Name VARCHAR(12) NOT NULL, -- 店員名稱
F_Phone VARCHAR(32) NOT NULL, -- 手機號碼
F_ICID VARCHAR(20) NULL, -- 身份證號碼
F_WeChat VARCHAR(20) NULL, -- 微信號
F_OpenID VARCHAR(100) NULL, -- 微信用戶的唯一標識。
F_Unionid VARCHAR(100) NULL, -- 只有將公眾號綁定到微信開放平台帳號后,才會出現該字段,同一用戶的unionid是唯一的。
F_pwdEncrypted VARCHAR(0) NULL, -- 公鑰加密后的用戶密碼
F_Salt VARCHAR(32) NOT NULL, -- 加鹽后的MD5值
F_PasswordExpireDate DATETIME NULL, -- 密碼有效期
F_IsFirstTimeLogin INT NOT NULL DEFAULT 1, -- 首次登錄成功?
F_ShopID INT NOT NULL, -- 門店ID
F_DepartmentID INT NOT NULL, -- 所屬的部門。默認為1
F_Status INT NOT NULL, -- 0,在職。1,離職。
F_CreateDatetime DATETIME NOT NULL DEFAULT now(), -- 創建時間
F_UpdateDatetime DATETIME NOT NULL DEFAULT now(), -- 修改時間
PRIMARY KEY (F_ID),
FOREIGN KEY (F_ShopID) REFERENCES T_Shop(F_ID),
FOREIGN KEY (F_DepartmentID) REFERENCES T_Department(F_ID)
-- FOREIGN KEY (F_IDInPOS) REFERENCES T_POS(F_ID) -- ,
-- UNIQUE KEY F_ICID (F_ICID),
-- UNIQUE KEY F_WeChat (F_WeChat)
-- UNIQUE KEY F_Phone (F_Phone) status為1的phone可以重新創建一個正常使用的staff。
)
ENGINE=InnoDB
DEFAULT CHARACTER SET='utf8' COLLATE='utf8_unicode_ci';
(2)角色表t_role
DROP TABLE IF EXISTS T_Role;
CREATE TABLE T_Role -- 角色
(
F_ID INT NOT NULL AUTO_INCREMENT, -- ID
F_Name VARCHAR(20) NOT NULL, -- 角色名稱
F_CreateDatetime DATETIME NOT NULL DEFAULT now(), -- 創建時間
F_UpdateDatetime DATETIME NOT NULL DEFAULT now(), -- 修改時間
PRIMARY KEY (F_ID)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET='utf8' COLLATE='utf8_unicode_ci';
(3)權限表t_permission
關鍵字段F_SP
DROP TABLE IF EXISTS T_Permission;
CREATE TABLE T_Permission -- 操作權限表
(
F_ID INT NOT NULL AUTO_INCREMENT, -- ID
F_SP VARCHAR(80) NOT NULL, -- 對應操作的SP
F_Name VARCHAR(20) NOT NULL, -- 操作名稱
F_Domain VARCHAR(16) NOT NULL , -- 領域名稱
F_Remark VARCHAR(32) NOT NULL, -- 操作備注
F_CreateDatetime DATETIME NOT NULL DEFAULT now(), -- 創建時間
PRIMARY KEY (F_ID)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET='utf8' COLLATE='utf8_unicode_ci';
(4)用戶角色表t_staffrole
DROP TABLE IF EXISTS T_StaffRole;
CREATE TABLE T_StaffRole -- 用戶角色表
(
F_ID INT NOT NULL AUTO_INCREMENT, -- ID
F_StaffID INT NOT NULL, -- 用戶ID
F_RoleID INT NOT NULL, -- 角色ID
PRIMARY KEY (F_ID),
FOREIGN KEY (F_StaffID ) REFERENCES T_Staff(F_ID),
FOREIGN KEY (F_RoleID ) REFERENCES T_Role(F_ID),
UNIQUE KEY F_StaffID(F_StaffID) -- ... 一個員工有且只有一個角色
)
ENGINE=InnoDB
DEFAULT CHARACTER SET='utf8' COLLATE='utf8_unicode_ci';
(5)角色權限表t_role_permission
DROP TABLE IF EXISTS T_Role_Permission;
CREATE TABLE T_Role_Permission -- 角色操作中間表
(
F_ID INT NOT NULL AUTO_INCREMENT, -- ID
F_RoleID INT NOT NULL, -- 角色ID
F_PermissionID INT NOT NULL, -- 操作ID
PRIMARY KEY (F_ID),
FOREIGN KEY (F_RoleID ) REFERENCES T_Role(F_ID),
FOREIGN KEY (F_PermissionID ) REFERENCES T_Permission(F_ID)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET='utf8' COLLATE='utf8_unicode_ci';
2、創建連接表的視圖
根據5張數據庫表創建用戶-角色-權限視圖v_staff_permission:
DROP VIEW IF EXISTS V_Staff_Permission; -- 用戶, 角色, 權限視圖
CREATE VIEW V_Staff_Permission
AS
SELECT s.F_ID AS StaffID, s.F_Name AS StaffName,r.F_ID AS RoleID, r.F_Name AS RoleName, p.F_SP, p.F_Name PermissionName, p.F_Remark FROM t_staff s
LEFT JOIN t_staffrole sr ON sr.F_StaffID = s.F_ID
JOIN t_role r ON sr.F_RoleID = r.F_ID
JOIN t_role_permission ap ON ap.F_RoleID = r.F_ID
JOIN t_permission p ON p.F_ID = ap.F_PermissionID
WHERE s.F_Status = 0;
3、調用視圖的存儲過程
存儲過程SP_Staff_RetrieveNPermission調用視圖,Java層調用存儲過程獲取所有的用戶權限:
DROP PROCEDURE IF EXISTS `SP_Staff_RetrieveNPermission`;
CREATE DEFINER=`root`@`localhost` PROCEDURE `SP_Staff_RetrieveNPermission` (
OUT iErrorCode INT,
OUT sErrorMsg VARCHAR(64),
IN iPageIndex INT,
IN iPageSize INT,
OUT iTotalRecord INT
)
BEGIN
DECLARE recordIndex INT;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SET iErrorCode := 3;
SET sErrorMsg := 'Êý¾Ý¿â´íÎó';
ROLLBACK;
END;
START TRANSACTION;
SET iPageIndex = iPageIndex - 1;
SET recordIndex = iPageIndex * iPageSize;
SET @i = 1;
SELECT @i:=@i+1 AS F_ID, StaffID, StaffName, RoleID, RoleName, F_SP, PermissionName, F_Remark
FROM V_Staff_Permission
ORDER BY F_ID DESC LIMIT recordIndex,iPageSize;
SELECT count(1) into iTotalRecord
FROM V_Staff_Permission;
SET iErrorCode := 0;
SET sErrorMsg := '';
COMMIT;
END;
4、Java層加載權限信息
StaffPermissionCache類用來緩存用戶權限信息,存放到hashtable中:
/** 權限的查詢需要支持根據staffID查詢,也要支持根據staffID和sp名查詢。后者用於檢查特定用戶有無特定權限,操作頻繁,需要快速查詢到 */
@Component("staffPermissionCache")
@Scope("prototype")
public class StaffPermissionCache extends BaseCache {
private Log logger = LogFactory.getLog(BaseCache.class);
/** key:staffID+sp名 <br />
* value:StaffPermission */
public Hashtable<String, BaseModel> ht;
@Resource
private StaffPermissionBO staffPermissionBO;
……
在程序啟動時,開始加載用戶權限信息:
/** 加載公共DB中的公司緩存和每個私有DB的普通緩存和同步緩存。 */
@PostConstruct
private void load() {
resolveCurrentEnvAndDomain();
……
staffPermissionCache.load(com.getDbName());
load調用doLoad方法:
public void load(String dbName) {
doLoad(dbName);
register(dbName);
}
doLoad方法調用staffPermissionBO的retrieveNObject,最后調用了SP_Staff_RetrieveNPermission存儲過程,獲取到所有的用戶和他們對應的權限信息,存放到List<?> ls:
@SuppressWarnings("unchecked")
protected void doLoad(String dbName) {
logger.info("加載緩存(" + sCacheName + ")...");
BaseModel b = getMasterModel(dbName);
DataSourceContextHolder.setDbName(dbName);
List<?> ls = getMasterBO().retrieveNObject(BaseBO.SYSTEM, BaseBO.INVALID_CASE_ID, b);
if (getMasterBO().getLastErrorCode() != EnumErrorCode.EC_NoError) {
logger.error("加載緩存(" + sCacheName + ")失敗!請重啟服務器!!錯誤信息:" + getMasterBO().printErrorInfo());
throw new RuntimeException(BaseModel.ERROR_Tag);
}
doLoadSlave(ls, dbName);
logger.info("加載緩存(" + sCacheName + ")成功!");
writeN((List<BaseModel>) ls); // 將DB的數據緩存進本對象的hashtable中
}
writeN((List<BaseModel>) ls) 方法把ls寫入到了hashtable容器中:
/** 清除所有舊的緩存,寫入新的數據 <br />
* TODO:目前存在的問題:load()中緩存加載時,並不能確定要加載多少個對象。 */
public void writeN(List<BaseModel> ls) {
System.out.println("writeN正在加鎖...." + lock.writeLock().getHoldCount());
lock.writeLock().lock();
try {
doWriteN(ls);
} catch (Exception e) {
logger.error("writeN異常:" + e.getMessage());
}
lock.writeLock().unlock();
System.out.println("writeN已經解鎖" + lock.writeLock().getHoldCount());
}
protected void doWriteN(List<BaseModel> ls) {
setCache(ls);
}
@Override
protected void setCache(List<BaseModel> list) {
listToHashtable(list);
}
以staffID+Sp為key,bm為Value存放到HashTable容器緩存起來:
@Override
protected Hashtable<String, BaseModel> listToHashtable(List<BaseModel> ls) {
if (ls == null) {
throw new RuntimeException("參數ls為null!");
}
ht = new Hashtable<String, BaseModel>(ls.size()); // 每一次設置緩存,都不會、也不能污染到本對象的副本中的ht數據成員
for (BaseModel bm : ls) {
ht.put(String.valueOf(((StaffPermission) bm).getStaffID()) + ((StaffPermission) bm).getSp(), bm);
}
return ht;
}
5、Java層判斷用戶是否有權限
所有用戶的對數據庫的操作都是通過調用存儲過程來實現的,在Action層調用BO層的方法的時候,已經指定了操作對應的存儲過程,如下圖的第二個參數BaseBO.CASE_ResetMyPassword,它對應了一個存儲過程:
Staff staffUpdated = (Staff) staffBO.updateObject(getStaffFromSession(session).getID(), BaseBO.INVALID_CASE_ID, staff);
調用過程如下:
@SuppressWarnings("static-access")
protected BaseModel update(int staffID, int iUseCaseID, BaseModel s) {
checkMapper();
if (!checkUpdatePermission(staffID, iUseCaseID, s)) {
lastErrorCode = EnumErrorCode.EC_NoPermission;
lastErrorMessage = "權限不足";
return null;
}
……
@Override
protected boolean checkUpdatePermission(int staffID, int iUseCaseID, BaseModel s) {
switch (iUseCaseID) {
case CASE_ResetMyPassword:
return checkStaffPermission(staffID, SP_Staff_ResetPassword);
case CASE_ResetOtherPassword:
return checkStaffPermission(staffID, SP_Staff_ResetPassword);
case CASE_Staff_Update_OpenidAndUnionid:
return checkStaffPermission(staffID, SP_Staff_Update_OpenidAndUnionid);
case CASE_Staff_Update_Unsubscribe:
return checkStaffPermission(staffID, SP_Staff_Update_Unsubscribe);
default:
return checkStaffPermission(staffID, SP_Staff_Update);
}
}
在checkStaffPermission方法檢查是否有權限:
/** 檢查1個staff有無權限permission */
protected boolean checkStaffPermission(int staffID, String permission) {
if (staffID == SYSTEM) {
return true;
}
StaffPermissionCache spc = (StaffPermissionCache) CacheManager.getCache(DataSourceContextHolder.getDbName(), EnumCacheType.ECT_StaffPermission);
ErrorInfo ecOut = new ErrorInfo();
StaffPermission sp = spc.read1(staffID, permission, ecOut);
if (sp == null) {
lastErrorMessage = "Staff(" + staffID + ")沒有權限(" + permission + ")";
logger.debug(lastErrorMessage);
return false;
}
return true;
}
在read1方法中,根據用戶ID和存儲過程拼接key,查找hashtable是否有這個key,從而判斷用戶是否有權限:
public StaffPermission read1(int staffID, String permissionName, ErrorInfo ecOut) {
StaffPermission bmToRead = null;
lock.readLock().lock();
String key = String.valueOf(staffID) + permissionName;
if (ht.containsKey(key)) {
bmToRead = (StaffPermission) ht.get(key);
ecOut.setErrorCode(EnumErrorCode.EC_NoError);
} else {
bmToRead = null;
ecOut.setErrorCode(EnumErrorCode.EC_NoSuchData);
logger.debug("無此權限。key=" + key);
}
lock.readLock().unlock();
return bmToRead;
}
6、可以使用Excel管理角色權限信息
使用excel可以方便管理角色權限信息:
后面幾個工作表配置角色的權限: