一套完整的系統許可權需要支援功能許可權和資料許可權,前面介紹了系統通過RBAC的許可權模型來實現功能的許可權控制,這裡我們來介紹,通過擴充套件Mybatis-Plus的外掛DataPermissionInterceptor實現資料許可權控制。
簡單介紹一下,所謂功能許可權,顧名思義是指使用者在系統中擁有對哪些功能操作的許可權控制,而資料許可權是指使用者在系統中能夠訪問哪些資料的許可權控制,資料許可權又分為行級資料許可權和列級資料許可權。
資料許可權基本概念:
- 行級資料許可權:以表結構為描述物件,一個使用者擁有對哪些資料的許可權,表示為對資料庫某個表整行的資料擁有許可權,例如按部門區分,某一行資料屬於某個部門,某個使用者只對此部門的資料擁有許可權,那麼該使用者擁有此行的資料許可權。
- 列級資料許可權:以表結構為描述物件,一個使用者可能只對某個表中的部分欄位擁有許可權,例如表中銀行卡、手機號等重要資訊只有高階使用者能夠查詢,而一些基本資訊,普通使用者就可以查詢,不同的使用者角色擁有的資料許可權不一樣。
實現方式:
- 行級資料許可權:
對行級資料許可權進行細分,以角色為標識的資料許可權,分為:
1、只能檢視本人資料;
2、只能檢視本部門資料;
3、只能檢視本部門及子部門資料;
4、可以檢視所有部門資料;
以使用者為標識的資料許可權,分為:
5、同一功能角色許可權擁有不同部門的資料許可權;
6、不同角色許可權擁有不同部門的資料許可權。
第1/2/3/4類的實現方式需要在角色列表對角色進行資料許可權配置,針對某一介面該角色擁有哪種資料許可權。
第5類的實現方式,需要在使用者列表進行配置,給使用者分配多個不同部門。
第6類的實現方式比較複雜,目前有市面上的大多數解決方案是:
1、在登入時,判斷使用者是否擁有多個部門,如果存在,那麼首先讓使用者選擇其所在的部門,登入後只對選擇的部門許可權進行操作;
2、針對不同部門建立不同的使用者及角色,登入時,選擇對應的賬號進行登入。
個人因秉承複雜的系統簡單化,儘量用低耦合的方式實現複雜功能的理念,更傾向於第二種方式,原因是:
1、系統實現方面減少複雜度,越複雜的判斷,越容易出問題,不僅僅在開發過程中,還在於後續系統的擴充套件和更新過程中。
2、對於工作量方面的取捨,一個人擁有多個部門不同許可權的方式屬於常用功能,但是並不普遍,也就是說在一家企業中,同一個使用者即是業務部門經理,又是財務部門經理的情況並不普遍,更多的是專人專職。這裡要和第5類做好區分,比如你是業務部門經理可能會管理多個部門,這種屬於許可權一致,只是擁有多個部門許可權,這屬於第5類。再比如一個總經理,可能會看到所有的業務、財務資料這屬於第4類。
所以這裡不會採取使用者登入後選擇部門的方式來判斷資料許可權。
- 列級資料許可權:
列級資料許可權的實現主要是針對某個角色能夠看到哪些欄位,不存在針對某個使用者給他特定欄位的情況,這種情況單獨建立一個角色即可,儘量採用類RBAC的方式來實現,不要使使用者直接和資料許可權關聯。列級資料許可權除了要考慮後臺取資料的問題,還要考慮到在介面上展示時,如果是一個表格,那麼沒有許可權的列需要根據資料許可權來判斷是否展示。這裡在配置介面就要考慮,在角色配置時,需要分為行級資料許可權和列級資料許可權進行不同的配置:行級資料許可權應該配置需要資料許可權控制的介面,資料許可權的型別(上面提到的1234);列級資料許可權除了需要配置上面提到的之外,還需要配置可以訪問的欄位或者排除訪問的欄位。
在資源管理配置資源關聯介面的資料許可權規則(t_sys_data_permission_role),通過RBAC的方式用角色和使用者關聯,在使用者管理配置使用者同角色的多個部門資料許可權,使用者直接和部門關聯(t_sys_data_permission_user)。系統資料許可權管理功能設計如下所示:
資料許可權表設計:
CREATE TABLE `t_sys_data_permission_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租戶id',
`user_id` bigint(20) NOT NULL COMMENT '使用者id',
`organization_id` bigint(20) NOT NULL COMMENT '機構id',
`status` tinyint(2) NULL DEFAULT 1 COMMENT '狀態 0禁用,1 啟用,',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '建立時間',
`creator` bigint(20) NULL DEFAULT NULL COMMENT '建立者',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新時間',
`operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
`del_flag` tinyint(2) NULL DEFAULT 0 COMMENT '1:刪除 0:不刪除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `t_sys_data_permission_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租戶id',
`resource_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '功能許可權id',
`data_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '資料許可權名稱',
`data_mapper_function` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '資料許可權對應的mapper方法全路徑',
`data_table_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '需要做資料許可權主表',
`data_table_alias` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '需要做資料許可權表的別名',
`data_column_exclude` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '資料許可權需要排除的欄位',
`data_column_include` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '資料許可權需要保留的欄位',
`inner_table_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '資料許可權表,預設t_sys_organization',
`inner_table_alias` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '資料許可權表的別名,預設organization',
`data_permission_type` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '1' COMMENT '資料許可權型別:1只能檢視本人 2只能檢視本部門 3只能檢視本部門及子部門 4可以檢視所有資料',
`custom_expression` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '自定義資料許可權(增加 where條件)',
`status` tinyint(2) NOT NULL DEFAULT 1 COMMENT '狀態 0禁用,1 啟用,',
`comments` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '備註',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '建立時間',
`creator` bigint(20) NULL DEFAULT NULL COMMENT '建立者',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新時間',
`operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
`del_flag` tinyint(2) NULL DEFAULT 0 COMMENT '1:刪除 0:不刪除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '資料許可權配置表' ROW_FORMAT = DYNAMIC;
資料許可權快取(Redis)設計:
- Redis Key:
多租戶模式:auth:tenant:data:permission:0(租戶):mapper_Mapper全路徑_type_資料許可權型別
普通模式:auth:data:permission:mapper_Mapper全路徑_type_資料許可權型別 - Redis Value:存放角色分配的DataPermissionEntity配置
資料許可權外掛在組裝SQL時,首先通過字首匹配查詢mapper的statementId是否在快取中,如果存在,那麼取出當前使用者的資料許可權型別,組裝好帶有資料許可權型別的DataPermission快取Key,從快取中取出資料許可權配置。
在設計角色時,除了需要給角色設定功能許可權之外,還要設定資料許可權型別,角色的資料許可權型別只能單選(1只能檢視本人 2只能檢視本部門 3只能檢視本部門及子部門 4可以檢視所有資料5自定義)
程式碼實現:
- 因DataPermissionInterceptor預設不支援修改selectItems,導致無法做到列級別的資料許可權,所以這裡自定義擴充套件DataPermissionInterceptor,使其支援列級許可權擴充套件
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class GitEggDataPermissionInterceptor extends DataPermissionInterceptor {
private GitEggDataPermissionHandler dataPermissionHandler;
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
if (!InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
mpBs.sql(this.parserSingle(mpBs.sql(), ms.getId()));
}
}
protected void processSelect(Select select, int index, String sql, Object obj) {
SelectBody selectBody = select.getSelectBody();
if (selectBody instanceof PlainSelect) {
PlainSelect plainSelect = (PlainSelect)selectBody;
this.processDataPermission(plainSelect, (String)obj);
} else if (selectBody instanceof SetOperationList) {
SetOperationList setOperationList = (SetOperationList)selectBody;
List<selectbody> selectBodyList = setOperationList.getSelects();
selectBodyList.forEach((s) -> {
PlainSelect plainSelect = (PlainSelect)s;
this.processDataPermission(plainSelect, (String)obj);
});
}
}
protected void processDataPermission(PlainSelect plainSelect, String whereSegment) {
this.dataPermissionHandler.processDataPermission(plainSelect, whereSegment);
}
}
- 自定義實現DataPermissionHandler資料許可權控制
@Component
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class GitEggDataPermissionHandler implements DataPermissionHandler {
@Value(("${tenant.enable}"))
private Boolean enable;
/**
* 註解方式預設關閉,這裡只是說明一種實現方式,實際使用時,使用配置的方式即可
*/
@Value(("${data-permission.annotation-enable}"))
private Boolean annotationEnable = false;
private final RedisTemplate redisTemplate;
public void processDataPermission(PlainSelect plainSelect, String mappedStatementId) {
try {
GitEggUser loginUser = GitEggAuthUtils.getCurrentUser();
// 1 當有資料許可權配置時才去判斷使用者是否有資料許可權控制
if (ObjectUtils.isNotEmpty(loginUser) && CollectionUtils.isNotEmpty(loginUser.getDataPermissionTypeList())) {
// 1 根據系統配置的資料許可權拼裝sql
StringBuffer statementSb = new StringBuffer();
if (enable)
{
statementSb.append(DataPermissionConstant.TENANT_DATA_PERMISSION_KEY).append(loginUser.getTenantId());
}
else
{
statementSb.append(DataPermissionConstant.DATA_PERMISSION_KEY);
}
String dataPermissionKey = statementSb.toString();
StringBuffer statementSbt = new StringBuffer(DataPermissionConstant.DATA_PERMISSION_KEY_MAPPER);
statementSbt.append(mappedStatementId).append(DataPermissionConstant.DATA_PERMISSION_KEY_TYPE);
String mappedStatementIdKey = statementSbt.toString();
DataPermissionEntity dataPermissionEntity = null;
for (String dataPermissionType: loginUser.getDataPermissionTypeList())
{
String dataPermissionUserKey = mappedStatementIdKey + dataPermissionType;
dataPermissionEntity = (DataPermissionEntity) redisTemplate.boundHashOps(dataPermissionKey).get(dataPermissionUserKey);
if (ObjectUtils.isNotEmpty(dataPermissionEntity)) {
break;
}
}
// mappedStatementId是否有配置資料許可權
if (ObjectUtils.isNotEmpty(dataPermissionEntity))
{
dataPermissionFilter(loginUser, dataPermissionEntity, plainSelect);
}
//預設不開啟註解,因每次查詢都遍歷註解,影響效能,直接選擇使用配置的方式實現資料許可權即可
else if(annotationEnable)
{
// 2 根據註解的資料許可權拼裝sql
Class<!--?--> clazz = Class.forName(mappedStatementId.substring(GitEggConstant.Number.ZERO, mappedStatementId.lastIndexOf(StringPool.DOT)));
String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + GitEggConstant.Number.ONE);
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
//當有多個時,這個方法可以獲取到
DataPermission[] annotations = method.getAnnotationsByType(DataPermission.class);
if (ObjectUtils.isNotEmpty(annotations) && method.getName().equals(methodName)) {
for (DataPermission dataPermission : annotations) {
String dataPermissionType = dataPermission.dataPermissionType();
for (String dataPermissionUser : loginUser.getDataPermissionTypeList()) {
if (ObjectUtils.isNotEmpty(dataPermission) && StringUtils.isNotEmpty(dataPermissionType)
&& dataPermissionUser.equals(dataPermissionType)) {
DataPermissionEntity dataPermissionEntityAnnotation = annotationToEntity(dataPermission);
dataPermissionFilter(loginUser, dataPermissionEntityAnnotation, plainSelect);
break;
}
}
}
}
}
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 構建過濾條件
*
* @param user 當前登入使用者
* @param plainSelect plainSelect
* @return 構建後查詢條件
*/
public static void dataPermissionFilter(GitEggUser user, DataPermissionEntity dataPermissionEntity, PlainSelect plainSelect) {
Expression expression = plainSelect.getWhere();
String dataPermissionType = dataPermissionEntity.getDataPermissionType();
String dataTableName = dataPermissionEntity.getDataTableName();
String dataTableAlias = dataPermissionEntity.getDataTableAlias();
String innerTableName = StringUtils.isNotEmpty(dataPermissionEntity.getInnerTableName()) ? dataPermissionEntity.getInnerTableName(): DataPermissionConstant.DATA_PERMISSION_TABLE_NAME;
String innerTableAlias = StringUtils.isNotEmpty(dataPermissionEntity.getInnerTableAlias()) ? dataPermissionEntity.getInnerTableAlias() : DataPermissionConstant.DATA_PERMISSION_TABLE_ALIAS_NAME;
List<string> organizationIdList = user.getOrganizationIdList();
// 列級資料許可權
String dataColumnExclude = dataPermissionEntity.getDataColumnExclude();
String dataColumnInclude = dataPermissionEntity.getDataColumnInclude();
List<string> includeColumns = new ArrayList<>();
List<string> excludeColumns = new ArrayList<>();
// 只包含這幾個欄位,也就是不是這幾個欄位的,直接刪除
if (StringUtils.isNotEmpty(dataColumnInclude))
{
includeColumns = Arrays.asList(dataColumnInclude.split(StringPool.COMMA));
}
// 需要排除這幾個欄位
if (StringUtils.isNotEmpty(dataColumnExclude))
{
excludeColumns = Arrays.asList(dataColumnExclude.split(StringPool.COMMA));
}
List<selectitem> selectItems = plainSelect.getSelectItems();
List<selectitem> removeItems = new ArrayList<>();
if (CollectionUtils.isNotEmpty(selectItems)
&& (CollectionUtils.isNotEmpty(includeColumns) || CollectionUtils.isNotEmpty(excludeColumns))) {
for (SelectItem selectItem : selectItems) {
// 暫不處理其他型別的selectItem
if (selectItem instanceof SelectExpressionItem) {
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
Alias alias = selectExpressionItem.getAlias();
if ((CollectionUtils.isNotEmpty(includeColumns) && !includeColumns.contains(alias.getName()))
|| (!CollectionUtils.isEmpty(excludeColumns) && excludeColumns.contains(alias.getName())))
{
removeItems.add(selectItem);
}
} else if (selectItem instanceof AllTableColumns) {
removeItems.add(selectItem);
}
}
if (CollectionUtils.isNotEmpty(removeItems))
{
selectItems.removeAll(removeItems);
plainSelect.setSelectItems(selectItems);
}
}
// 行級資料許可權
// 查詢使用者機構和子機構的資料,這裡是使用where條件新增子查詢的方式來實現的,這樣的實現方式好處是不需要判斷Update,Insert還是Select,都是通用的,缺點是效能問題。
if (DataPermissionTypeEnum.DATA_PERMISSION_ORG_AND_CHILD.getLevel().equals(dataPermissionType)) {
// 如果是table的話,那麼直接加inner,如果不是,那麼直接在where條件里加子查詢
if (plainSelect.getFromItem() instanceof Table)
{
Table fromTable = (Table)plainSelect.getFromItem();
//資料主表
Table dataTable = null;
//inner資料許可權表
Table innerTable = null;
if (fromTable.getName().equalsIgnoreCase(dataTableName))
{
dataTable = (Table)plainSelect.getFromItem();
}
// 如果是查詢,這裡使用inner join關聯過濾,不使用子查詢,因為join不需要建立臨時表,因此速度比子查詢快。
List<join> joins = plainSelect.getJoins();
boolean hasPermissionTable = false;
if (CollectionUtils.isNotEmpty(joins)) {
Iterator joinsIterator = joins.iterator();
while(joinsIterator.hasNext()) {
Join join = (Join)joinsIterator.next();
// 判斷join裡面是否存在t_sys_organization表,如果存在,那麼直接使用,如果不存在則新增
FromItem rightItem = join.getRightItem();
if (rightItem instanceof Table) {
Table table = (Table)rightItem;
// 判斷需要inner的主表是否存在
if (null == dataTable && table.getName().equalsIgnoreCase(dataTableName))
{
dataTable = table;
}
// 判斷需要inner的表是否存在
if (table.getName().equalsIgnoreCase(innerTableName))
{
hasPermissionTable = true;
innerTable = table;
}
}
}
}
//如果沒有找到資料主表,那麼直接丟擲異常
if (null == dataTable)
{
throw new BusinessException("在SQL語句中沒有找到資料許可權配置的主表,資料許可權過濾失敗。");
}
//如果不存在這個table,那麼新增一個innerjoin
if (!hasPermissionTable)
{
innerTable = new Table(innerTableName).withAlias(new Alias(innerTableAlias, false));
Join join = new Join();
join.withRightItem(innerTable);
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column(dataTable, DataPermissionConstant.DATA_PERMISSION_ORGANIZATION_ID));
equalsTo.setRightExpression(new Column(innerTable, DataPermissionConstant.DATA_PERMISSION_ID));
join.withOnExpression(equalsTo);
plainSelect.addJoins(join);
}
EqualsTo equalsToWhere = new EqualsTo();
equalsToWhere.setLeftExpression(new Column(innerTable, DataPermissionConstant.DATA_PERMISSION_ID));
equalsToWhere.setRightExpression(new LongValue(user.getOrganizationId()));
Function function = new Function();
function.setName(DataPermissionConstant.DATA_PERMISSION_FIND_IN_SET);
function.setParameters(new ExpressionList(new LongValue(user.getOrganizationId()) , new Column(innerTable, DataPermissionConstant.DATA_PERMISSION_ANCESTORS)));
OrExpression orExpression = new OrExpression(equalsToWhere, function);
//判斷是否有資料許可權,如果有資料許可權配置,那麼新增資料許可權的機構列表
if(CollectionUtils.isNotEmpty(organizationIdList))
{
for (String organizationId : organizationIdList)
{
EqualsTo equalsToPermission = new EqualsTo();
equalsToPermission.setLeftExpression(new Column(innerTable, DataPermissionConstant.DATA_PERMISSION_ID));
equalsToPermission.setRightExpression(new LongValue(organizationId));
orExpression = new OrExpression(orExpression, equalsToPermission);
Function functionPermission = new Function();
functionPermission.setName(DataPermissionConstant.DATA_PERMISSION_FIND_IN_SET);
functionPermission.setParameters(new ExpressionList(new LongValue(organizationId) , new Column(innerTable,DataPermissionConstant.DATA_PERMISSION_ANCESTORS)));
orExpression = new OrExpression(orExpression, functionPermission);
}
}
expression = ObjectUtils.isNotEmpty(expression) ? new AndExpression(expression, new Parenthesis(orExpression)) : orExpression;
plainSelect.setWhere(expression);
}
else
{
InExpression inExpression = new InExpression();
inExpression.setLeftExpression(buildColumn(dataTableAlias, DataPermissionConstant.DATA_PERMISSION_ORGANIZATION_ID));
SubSelect subSelect = new SubSelect();
PlainSelect select = new PlainSelect();
select.setSelectItems(Collections.singletonList(new SelectExpressionItem(new Column(DataPermissionConstant.DATA_PERMISSION_ID))));
select.setFromItem(new Table(DataPermissionConstant.DATA_PERMISSION_TABLE_NAME));
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column(DataPermissionConstant.DATA_PERMISSION_ID));
equalsTo.setRightExpression(new LongValue(user.getOrganizationId()));
Function function = new Function();
function.setName(DataPermissionConstant.DATA_PERMISSION_FIND_IN_SET);
function.setParameters(new ExpressionList(new LongValue(user.getOrganizationId()) , new Column(DataPermissionConstant.DATA_PERMISSION_ANCESTORS)));
OrExpression orExpression = new OrExpression(equalsTo, function);
//判斷是否有資料許可權,如果有資料許可權配置,那麼新增資料許可權的機構列表
if(CollectionUtils.isNotEmpty(organizationIdList))
{
for (String organizationId : organizationIdList)
{
EqualsTo equalsToPermission = new EqualsTo();
equalsToPermission.setLeftExpression(new Column(DataPermissionConstant.DATA_PERMISSION_ID));
equalsToPermission.setRightExpression(new LongValue(organizationId));
orExpression = new OrExpression(orExpression, equalsToPermission);
Function functionPermission = new Function();
functionPermission.setName(DataPermissionConstant.DATA_PERMISSION_FIND_IN_SET);
functionPermission.setParameters(new ExpressionList(new LongValue(organizationId) , new Column(DataPermissionConstant.DATA_PERMISSION_ANCESTORS)));
orExpression = new OrExpression(orExpression, functionPermission);
}
}
select.setWhere(orExpression);
subSelect.setSelectBody(select);
inExpression.setRightExpression(subSelect);
expression = ObjectUtils.isNotEmpty(expression) ? new AndExpression(expression, new Parenthesis(inExpression)) : inExpression;
plainSelect.setWhere(expression);
}
}
// 只查詢使用者擁有機構的資料,不包含子機構
else if (DataPermissionTypeEnum.DATA_PERMISSION_ORG.getLevel().equals(dataPermissionType)) {
InExpression inExpression = new InExpression();
inExpression.setLeftExpression(buildColumn(dataTableAlias, DataPermissionConstant.DATA_PERMISSION_ORGANIZATION_ID));
ExpressionList expressionList = new ExpressionList();
List<expression> expressions = new ArrayList<>();
expressions.add(new LongValue(user.getOrganizationId()));
if(CollectionUtils.isNotEmpty(organizationIdList))
{
for (String organizationId : organizationIdList)
{
expressions.add(new LongValue(organizationId));
}
}
expressionList.setExpressions(expressions);
inExpression.setRightItemsList(expressionList);
expression = ObjectUtils.isNotEmpty(expression) ? new AndExpression(expression, new Parenthesis(inExpression)) : inExpression;
plainSelect.setWhere(expression);
}
// 只能查詢個人資料
else if (DataPermissionTypeEnum.DATA_PERMISSION_SELF.getLevel().equals(dataPermissionType)) {
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(buildColumn(dataTableAlias, DataPermissionConstant.DATA_PERMISSION_SELF));
equalsTo.setRightExpression(new StringValue(String.valueOf(user.getId())));
expression = ObjectUtils.isNotEmpty(expression) ? new AndExpression(expression, new Parenthesis(equalsTo)) : equalsTo;
plainSelect.setWhere(expression);
}
//當型別為檢視所有資料時,不處理
// if (DataPermissionTypeEnum.DATA_PERMISSION_ALL.getType().equals(dataPermissionType)) {
//
// }
// 自定義過濾語句
else if (DataPermissionTypeEnum.DATA_PERMISSION_CUSTOM.getLevel().equals(dataPermissionType)) {
String customExpression = dataPermissionEntity.getCustomExpression();
if (StringUtils.isEmpty(customExpression))
{
throw new BusinessException("沒有配置自定義表示式");
}
try {
Expression expressionCustom = CCJSqlParserUtil.parseCondExpression(customExpression);
expression = ObjectUtils.isNotEmpty(expression) ? new AndExpression(expression, new Parenthesis(expressionCustom)) : expressionCustom;
plainSelect.setWhere(expression);
} catch (JSQLParserException e) {
throw new BusinessException("自定義表示式配置錯誤");
}
}
}
/**
* 構建Column
*
* @param dataTableAlias 表別名
* @param columnName 欄位名稱
* @return 帶表別名欄位
*/
public static Column buildColumn(String dataTableAlias, String columnName) {
if (StringUtils.isNotEmpty(dataTableAlias)) {
columnName = dataTableAlias + StringPool.DOT + columnName;
}
return new Column(columnName);
}
/**
* 註解轉為實體類
* @param annotation 註解
* @return 實體類
*/
public static DataPermissionEntity annotationToEntity(DataPermission annotation) {
DataPermissionEntity dataPermissionEntity = new DataPermissionEntity();
dataPermissionEntity.setDataPermissionType(annotation.dataPermissionType());
dataPermissionEntity.setDataColumnExclude(annotation.dataColumnExclude());
dataPermissionEntity.setDataColumnInclude(annotation.dataColumnInclude());
dataPermissionEntity.setDataTableName(annotation.dataTableName());
dataPermissionEntity.setDataTableAlias(annotation.dataTableAlias());
dataPermissionEntity.setInnerTableName(annotation.innerTableName());
dataPermissionEntity.setInnerTableAlias(annotation.innerTableAlias());
dataPermissionEntity.setCustomExpression(annotation.customExpression());
return dataPermissionEntity;
}
@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
return null;
}
- 系統啟動時初始化資料許可權配置到Redis
@Override
public void initDataRolePermissions() {
List<datapermissionroledto> dataPermissionRoleList = dataPermissionRoleMapper.queryDataPermissionRoleListAll();
// 判斷是否開啟了租戶模式,如果開啟了,那麼角色許可權需要按租戶進行分類儲存
if (enable) {
Map<long, list<datapermissionroledto="">> dataPermissionRoleListMap =
dataPermissionRoleList.stream().collect(Collectors.groupingBy(DataPermissionRoleDTO::getTenantId));
dataPermissionRoleListMap.forEach((key, value) -> {
// auth:tenant:data:permission:0
String redisKey = DataPermissionConstant.TENANT_DATA_PERMISSION_KEY + key;
redisTemplate.delete(redisKey);
addDataRolePermissions(redisKey, value);
});
} else {
redisTemplate.delete(DataPermissionConstant.DATA_PERMISSION_KEY);
// auth:data:permission
addDataRolePermissions(DataPermissionConstant.DATA_PERMISSION_KEY, dataPermissionRoleList);
}
}
private void addDataRolePermissions(String key, List<datapermissionroledto> dataPermissionRoleList) {
Map<string, datapermissionentity=""> dataPermissionMap = new TreeMap<>();
Optional.ofNullable(dataPermissionRoleList).orElse(new ArrayList<>()).forEach(dataPermissionRole -> {
String dataRolePermissionCache = new StringBuffer(DataPermissionConstant.DATA_PERMISSION_KEY_MAPPER)
.append(dataPermissionRole.getDataMapperFunction()).append(DataPermissionConstant.DATA_PERMISSION_KEY_TYPE)
.append(dataPermissionRole.getDataPermissionType()).toString();
DataPermissionEntity dataPermissionEntity = BeanCopierUtils.copyByClass(dataPermissionRole, DataPermissionEntity.class);
dataPermissionMap.put(dataRolePermissionCache, dataPermissionEntity);
});
redisTemplate.boundHashOps(key).putAll(dataPermissionMap);
}
資料許可權配置指南:
- 資料許可權名稱:自定義一個名稱,方便查詢和區分
- Mapper全路徑: Mapper路徑配置到具體方法名稱,例:com.gitegg.service.system.mapper.UserMapper.selectUserList
- 資料許可權型別:
只能檢視本人(實現原理是在查詢條件新增資料表的creator條件)
只能檢視本部門 (實現原理是在查詢條件新增資料表的部門條件)
只能檢視本部門及子部門 (實現原理是在查詢條件新增資料表的部門條件)
可以檢視所有資料(不處理)
自定義(新增where子條件)
註解配置資料許可權配置指南:
/**
* 查詢使用者列表
* @param page
* @param user
* @return
*/
@DataPermission(dataTableName = "t_sys_organization_user", dataTableAlias = "organizationUser", dataPermissionType = "3", innerTableName = "t_sys_organization", innerTableAlias = "orgDataPermission")
@DataPermission(dataTableName = "t_sys_organization_user", dataTableAlias = "organizationUser", dataPermissionType = "2", innerTableName = "t_sys_organization", innerTableAlias = "orgDataPermission")
@DataPermission(dataTableName = "t_sys_organization_user", dataTableAlias = "organizationUser", dataPermissionType = "1", innerTableName = "t_sys_organization", innerTableAlias = "orgDataPermission")
Page<userinfo> selectUserList(Page<userinfo> page, @Param("user") QueryUserDTO user);
行級資料許可權配置:
資料主表:主資料表,用於資料操作時的主表,例如SQL語句時的主表
資料主表別名:主資料表的別名,用於和資料許可權表進行inner join操作
資料許可權表:用於inner join的資料許可權表,主要用於使用ancestors欄位查詢所有子組織機構
資料許可權表別名:用於和主資料表進行inner join
列級資料許可權配置:
排除的欄位:配置沒有許可權檢視的欄位,需要排除這些欄位
保留的欄位:配置有許可權檢視的欄位,只保留這些欄位
備註:
- 此資料許可權設計較靈活,也較複雜,有些簡單應用場景的系統可能根本用不到,只需配置行級資料許可權即可。
- Mybatis-Plus的外掛DataPermissionInterceptor使用說明 https://gitee.com/baomidou/mybatis-plus/issues/I37I90
- update,insert邏輯說明:inner時只支援正常查詢,及inner查詢,不支援子查詢,update,insert,子查詢等直接使用新增子查詢的方式實現資料許可權
- 還有在這裡說明一下,在我們實際業務開發過程中,只能檢視本人資料的資料許可權,一般不會通過系統來配置,而是在業務程式碼編寫過程中就 會實現,比如查詢個人訂單介面,那麼個人使用者id肯定是介面的入參,在介面被請求的時候,只需要通過我們自定義的方法獲取到當前登入使用者,然後作為引數傳入即可。這種對於個人資料的資料許可權,通過業務程式碼來實現會更加方便和安全,且沒有太多的工作量,方便理解也容易擴充套件。