數據權限管理中心
由於本人大多數項目都是使用MyBatis,也是使用MyBatis攔截器進行 分頁處理,所以技術上也直接選擇從攔截器入手。
需求場景
第一種場景:行級數據處理
原Sql: select id,username,region from sys_user;
需要封裝成: select * from( select id,username,region from sys_user )where 1=1 and region like "3210%";
注:用戶只能查詢當前所屬市及下屬地市數據其中like部分可以爲動態參數(詳情如下)。
此場景下還有以下情況: #判斷 select * from (select id,username,region from sys_user) where 1=1 and region !=320101; #枚舉 select * from (select id,username,region from sys_user) where 1=1 and region in (320101,320102,320103); ...
第二種場景:列級數據處理
原sql: select id,username,region from sys_user ; #用戶A可以看到 id,username,region #用戶B只能查看 id,username 的值,region的值沒有權限查看。
技術實現
MyBatis攔截器
在編寫MyBatis攔截器之前,先了解MyBatis的攔截目標方法:
- Executor(updaqte,query,flushStatements,commit,rollback,getTransaction,close,isClosed)
- ParameterHandler(getParameterObject,setParameters)
- StatementHandler(prepare,parameterize,batch,update,query)
- ResultSetHandler(handleResultSets,handleOutputParameters)
這裏選擇StatementHandler的prepare方法作爲sql執行之前的攔截Sql封裝,使用ResultSetHandler的handResultSets方法作爲Sql執行之後的結果攔截過濾。
SQL執行前
Preparameterceptor.java:
/**
*
* @ClassName: Preparamrterceptor.java
* @Description: MyBatis數據攔截器
* @author 龍隊今天贏了嗎
* @date 2018年8月20日 下午2:27:14
*
*/
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare",args={
Connection.class, Integer.class})
})
@Component
public class PrepareInterceptor implements Interceptor {
//日誌
private static final Logger log = LoggerFactory.getLogger(PrepareInterceptor.class);
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {}
@Override
public Object intercept(Invocation invocation) throws Throwable {
if(log.isInfoEnabled()){
log.info("進入 PrepareInterceptor 攔截器...");
}
if(invocation.getTarget() instanceof RoutingStatementHandler) {
RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate");
//通過反射獲取delegate父類BaseStatementHandler的mappedStatement屬性
MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement");
//千萬不能用下面註釋的這個方法,會造成對象丟失,以致轉換失敗
//MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
PermissionAop permissionAop = PermissionUtils.getPermissionByDelegate(mappedStatement);
if(permissionAop == null){
if(log.isInfoEnabled()){
log.info("數據權限放行...");
}
return invocation.proceed();
}
if(log.isInfoEnabled()){
log.info("數據權限處理【拼接SQL】...");
}
BoundSql boundSql = delegate.getBoundSql();
ReflectUtil.setFieldValue(boundSql, "sql", permissionSql(boundSql.getSql()));
}
return invocation.proceed();
}
/**
*
* @Description: 權限SQL包裝
* @author 龍隊今天贏了嗎
* @date 2018年8月20日 下午2:27:14
*
*/
protected String permissionSql(String sql) {
StringBuilder sbSql = new StringBuilder(sql);
String userMethodPath = PermissionConfig.getConfig("permission.client.userid.method");
//當前登錄人
String userId = (String)ReflectUtil.reflectByPath(userMethodPath);
//如果用戶爲 1 則只能查詢第一條
if("1".equals(userId)){
//sbSql = sbSql.append(" limit 1 ");
//如果有動態參數 regionCd
if(true){
String premission_param = "regionCd";
//select * from (select id,name,region_cd from sys_exam ) where region_cd like '${}%'
String methodPath = PermissionConfig.getConfig("permission.client.params." + premission_param);
String regionCd = (String)ReflectUtil.reflectByPath(methodPath);
sbSql = new StringBuilder("select * from (").append(sbSql).append(" ) s where s.regionCd like concat("+ regionCd +",'%') ");
}
}
return sbSql.toString();
}
}
SQL執行後
ResultInterceptor.java
/**
*
* @ClassName: ResultInterceptor.java
* @Description: MyBatis數據權限攔截器 - handleResultSets 對結果進行過濾
* @author 龍隊今天贏了嗎
* @date 2018年8月20日 下午2:27:14
*
*/
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args={Statement.class})
})
@Component
public class ResultInterceptor implements Interceptor {
//日誌
private static final Logger log = LoggerFactory.getLogger(ResultInterceptor.class);
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {}
@Override
public Object intercept(Invocation invocation) throws Throwable {
if(log.isInfoEnabled()){
log.info("進入 ResultInterceptor 攔截器...");
}
ResultSetHandler resultSetHandler1 = (ResultSetHandler) invocation.getTarget();
//通過java反射獲得mappedStatement屬性值
//可以獲得mybatis裏的resultype
MappedStatement mappedStatement = (MappedStatement)ReflectUtil.getFieldValue(resultSetHandler1, "mappedStatement");
//獲取切面對象
PermissionAop permissionAop = PermissionUtils.getPermissionByDelegate(mappedStatement);
//執行請求方法,並將所得結果保存到result中
Object result = invocation.proceed();
if(permissionAop != null) {
if (result instanceof ArrayList) {
ArrayList resultList = (ArrayList) result;
for (int i = 0; i < resultList.size(); i++) {
Object oi = resultList.get(i);
Class c = oi.getClass();
Class[] types = {String.class};
Method method = c.getMethod("setRegionCd", types);
// 調用obj對象的 method 方法
method.invoke(oi, "");
if(log.isInfoEnabled()){
log.info("數據權限處理【過濾結果】...");
}
}
}
}
return result;
}
}
注:其中PermissionAOP爲dao層自定義切面,用於控制是否用數據權限過濾。
- 如何在攔截器獲取dao層註解內容:不同方法的攔截器獲取方法稍微有所區別,具體如上(PrepareInterceptor.java與ResultInterceptor.java)。
- 如何獲取登錄人標識:不同框架不同項目獲取當前登錄人的方法不盡相同,可以通過配置的方法動態將獲取當前登錄人的方法傳遞給權限中心。配置文件添加:
然後利用Java反射機制,觸發getUserId()方法。# 客戶端獲取當前登錄人標識 permission.client.userid.method=com.raising.sc.permission.example.util.UserUtils.getUserId
- 如何傳遞動態參數:比如用戶A只能查詢自己單位以及下屬單位的所有數據;配置的where部分的sql如下:
然後通過PrepareInterceptor.java讀取以上sql,並且通過數據庫或者配置文件中設置的參數(orgCd)相關聯的方法(類似獲取當前登錄人標識的方法),提前在權限參數(orgCd)配置好對用的方法路徑、數值類型、返回類型等。配置文件或者數據庫獲取到orgCd對用的方法路徑:org_cd like concat(${orgCd},'%')
這樣就能簡單傳遞動態參數了。com.raising.sc.permission.example.util.UserUtils.getRegionCdByUserId
- 需要考慮到與sql分頁的優先級。
從產品來說,此模塊應該有以下幾個部分組成:
- foruo-permission-admin數據權限管理平臺
- foruo-permission-server數據權限服務端(提供權限相關接口)
- foruo-permission-client數據權限客戶端(封裝API)
- 應用鏈接邏輯圖的其他部分。
結合 應用鏈路邏輯圖 即可完成此模塊內容。