一套完整的系統權限需要支持功能權限和數據權限,前面介紹了系統通過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肯定是接口的入參,在接口被請求的時候,只需要通過我們自定義的方法獲取到當前登錄用戶,然后作為參數傳入即可。這種對於個人數據的數據權限,通過業務代碼來實現會更加方便和安全,且沒有太多的工作量,方便理解也容易擴展。