著作權說明: 本文由博主keep丶原創,轉載請注明出處,
原文地址: https://blog.csdn.net/qq_38688267/article/details/114134664
文章目錄
- 背景介紹
- 效果演示
- 核心代碼介紹
- 需要限制的方法標記和獲取
- 實作標記方法的攔截
- 實作SQL修改
- 源代碼分享
- 總結
背景介紹
??眾所周知,系統權限分為功能權限和資料權限,功能權限決定用戶能訪問哪些界面有哪些功能,資料權限則決定用戶能看到哪些資料,絕大部分系統都會用到這個功能,可以說是非常常見了,
??資料權限限制該怎么做呢? 一般情況下大家都是在需要限制的sql中加上該用戶的權限條件,作者之前也是這么實作的,
??雖然當時把這些權限條件SQL都封裝了,每次使用的時候只要參考就行,但是還是感覺很麻煩,就想著能不能讓程式來幫我實作這些資料權限限制SQL的注入呢?(我可真是個喜歡"偷懶"的小聰明呢)
??有了這個想法,作者就開始搗鼓了,經過一陣搗鼓后,作者封裝的基于Mybatis的 通用資料權限限制SQL工具V1.0 版本總算搗鼓出來了!
?
?
效果演示


??看完上面的效果,各位是不是很奇怪? sys_role這張表怎么來的?df_als別名怎么來的?ON sr.tenant_id = df_als.tenant_id怎么來的?sr.id IN ('asdf', 'asdff')又是怎么來的?
??全都是配置出來的! 為了足夠通用,很多東西都是可配的,如:
- 哪些表、方法、類需要限制,哪些不用可配
- 用哪些表來限制可配
- 關聯關系欄位、條件欄位可配
- where條件資料值來源可配
??還有更多:
- 可配置多個SQL處理器
- 多個處理器執行策略可配
- …
?
?
核心代碼介紹
要實作這個功能,有幾個要點:
- 需要標記并獲取哪些是需要限制的方法
- 實作上述方法的攔截
- 實作SQL修改
下面一一為大家介紹作者的實作方式,
?
需要限制的方法標記和獲取
??標記很簡單,用個注解就好了,獲取作者利用了mybatis的mapper掃描作業,重寫其方法讓其順便幫我們檢查下是否帶有標記注解,
/**
* 資料權限限制SqlInjector
* <p>
* 重寫mp的mapper掃描邏輯
* 在掃描mapper后檢查是否需要資料權限限制并快取這些資訊
*
* @author zzf
* @date 2021/2/5 15:02
*/
@Slf4j
public class DataAuthSqlInjector extends DefaultSqlInjector {
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
String resource = ResourceUtils.getResourceKey(mapperClass);
Set<String> needAuthMethodSet = new HashSet<>();
Set<String> notNeedAuthMethodSet = new HashSet<>();
if (mapperClass.isAnnotationPresent(DataAuth.class)) {
DataAuthCache.addNeedAuthResource(resource);
for (Method method : mapperClass.getMethods()) {
needAuthMethodSet.add(method.getName());
}
}
for (Method method : mapperClass.getMethods()) {
if (method.isAnnotationPresent(DataAuth.class)) {
needAuthMethodSet.add(method.getName());
}
if (method.isAnnotationPresent(DataAuthIgnore.class)) {
notNeedAuthMethodSet.add(method.getName());
}
}
if (needAuthMethodSet.size() > 0) {
DataAuthCache.addNeedAuthMethod(resource, needAuthMethodSet);
DataAuthCache.addResource2EntityClassMap(resource, extractModelClass(mapperClass));
}
if (notNeedAuthMethodSet.size() > 0) {
DataAuthCache.addNotNeedAuthMethod(resource, notNeedAuthMethodSet);
}
super.inspectInject(builderAssistant, mapperClass);
}
}
?
實作標記方法的攔截
??攔截也比較簡單,我們直接實作Mybatis Plus提供的攔截介面:
/**
* 資料權限攔截器
*
* @author zzf
* @date 11:05 2021/2/4
*/
@Slf4j
public class DataAuthInterceptor implements InnerInterceptor {
//限制策略
private final DataAuthStrategy dataAuthStrategy;
private final Set<String> needAuthId = new TreeSet<>();
private final Set<String> notNeedAuthId = new TreeSet<>();
public DataAuthInterceptor(DataAuthStrategy dataAuthStrategy) {
this.dataAuthStrategy = dataAuthStrategy;
}
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
String id = ms.getId();
String resource = ResourceUtils.getResourceKey(ms.getResource());
String methodName;
if (id.contains(".")) {
methodName = id.substring(id.lastIndexOf(".") + 1);
} else {
methodName = id;
}
if (needAuthCheck(resource, methodName, id)) {
log.warn("sql before parsed: " + boundSql.getSql());
Class<?> entityClass = DataAuthCache.getEntityClassByResource(resource);
try {
Select selectStatement = (Select) CCJSqlParserUtil.parse(boundSql.getSql());
dataAuthStrategy.doParse(selectStatement, entityClass);
ReflectUtil.setFieldValue(boundSql, "sql", selectStatement.toString());
log.warn("sql after parsed: " + boundSql.getSql());
} catch (JSQLParserException e) {
throw new SQLException("sql role limit fail: sql parse fail.");
}
}
}
/**
* 判斷是否需要資料權限限制
*/
private boolean needAuthCheck(String resource, String methodName, String id) {
if (needAuthId.contains(id)) {
return true;
}
if (notNeedAuthId.contains(id)) {
return false;
}
boolean result;
if (DataAuthCache.isNeedAuthResource(resource)) {
result = !DataAuthCache.isNotNeedAuthMethod(resource, methodName);
} else {
result = DataAuthCache.isNeedAuthMethod(resource, methodName);
}
if (result) {
needAuthId.add(id);
} else {
notNeedAuthId.add(id);
}
return result;
}
}
??
實作SQL修改
??自己手寫SQL修改是不太現實的,要考慮的情況太多了,作者使用JSqlParser工具來進行SQL修改,部分代碼如下:
/**
* 進行資料權限限制的SQL處理
*/
public SqlHandlerResult doParse(Select selectStatement, Class<?> entityClass) {
T data = dataGetter.getData();
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
//判斷資料是否為空,且為空時是否處理
if (data == null) {
if (parseIfDataAbsent()) {
//空資料權限處理
EqualsTo equalsTo = new EqualsTo();
equalsTo.setRightExpression(new LongValue(1));
equalsTo.setLeftExpression(new LongValue(0));
//在where子句中添加 1 = 0 條件, 并 and 原有條件
AndExpression andExpression = new AndExpression(equalsTo, selectBody.getWhere());
selectBody.setWhere(andExpression);
}
return SqlHandlerResult.nonData(parseIfDataAbsent());
} else {
if (dataIsFull(data)) {
// 擁有全部權限,此處不對SQL做任何處理
return SqlHandlerResult.hadAll();
} else {
// 正常處理
Table fromItem = (Table) selectBody.getFromItem();
aliasHandle(selectBody, fromItem);
joinHandle(entityClass, selectBody, fromItem);
whereHandle(selectBody);
}
}
return SqlHandlerResult.handle();
}
/**
* 別名處理
* 如果from的表沒有別名,手動給他加個別名,并給所有 查詢列和 where條件中的列增加別名前綴
*/
public void aliasHandle(PlainSelect selectBody, Table fromItem) {
if (fromItem.getAlias() == null || fromItem.getAlias().getName() == null) {
fromItem.setAlias(new Alias(DataAuthConstants.DEFAULT_ALIAS));
selectBody.getSelectItems().forEach(s ->
s.accept(new ExpressionVisitorAdapter() {
@Override
public void visit(Column column) {
column.setTable(fromItem);
}
})
);
Expression where = selectBody.getWhere();
if (where != null) {
where.accept(new ExpressionVisitorAdapter() {
@Override
public void visit(Column column) {
column.setTable(fromItem);
}
});
}
}
}
/**
* JOIN子句處理
* <p>
* 將資料限制表通過JOIN的方式加入
*/
public void joinHandle(Class<?> entityClass, PlainSelect selectBody, Table fromItem) {
// 假如要實作 LEFT JOIN t2 ON t2.id2 = t1.id1
Table limitTable = getLimitTable();
Join join = new Join();// JOIN
join.setLeft(true);// LEFT JOIN
join.setRightItem(limitTable);// LEFT JOIN t2
EqualsTo equalsTo = new EqualsTo();// =
Column limitRelationColumn = new Column(relationColumnName);// id2
limitRelationColumn.setTable(limitTable);// t2.id2
Column targetRelationColumn = new Column(DataAuthCache.getFieldName(getId(), entityClass));// id1
targetRelationColumn.setTable(fromItem);// t1.id1
equalsTo.setLeftExpression(limitRelationColumn);
equalsTo.setRightExpression(targetRelationColumn);// t2.id2 = t1.id1
join.setOnExpression(equalsTo);// LEFT JOIN t2 ON t2.id2 = t1.id1
List<Join> joins = selectBody.getJoins();
if (joins == null) {
selectBody.setJoins(Collections.singletonList(join));
} else {
joins.add(join);
}
}
/**
* WHERE子句處理
* <p>
* 添加資料權限條件
*/
public void whereHandle(PlainSelect selectBody) {
Column whereColumn = new Column(whereColumnName);
whereColumn.setTable(getLimitTable());
setOperatorValue();
AndExpression andExpression = new AndExpression(operator, selectBody.getWhere());
selectBody.setWhere(andExpression);
}
protected Table getLimitTable() {
Table limitTable = new Table(this.tableName);
limitTable.setAlias(new Alias(this.tableAlias));
return limitTable;
}
源代碼分享
專案地址:https://gitee.com/zengzefeng/easy-frame
?
總結
??該功能只是初版,肯定還有很多不足和考慮不周的地方,還請各位大佬多多指點,后續還會繼續優化迭代,希望能打造成一個spring-boot-starter,哈哈,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/264191.html
標籤:其他
下一篇:qsort庫函式和模擬實作
