feat(extension/datapermission): 新增数据权限模块

This commit is contained in:
2024-09-26 23:27:43 +08:00
parent 04e0b4b1cc
commit 7666d56019
16 changed files with 320 additions and 135 deletions

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter-extension-datapermission</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-extension-datapermission-core</artifactId>
<description>ContiNew Starter 扩展模块 - 数据权限 - 核心模块</description>
</project>

View File

@@ -0,0 +1,66 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.datapermission.annotation;
import java.lang.annotation.*;
/**
* 数据权限注解
*
* @author Charles7c
* @since 1.1.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {
/**
* 表别名
*/
String tableAlias() default "";
/**
* ID
*/
String id() default "id";
/**
* 部门 ID
*/
String deptId() default "dept_id";
/**
* 用户 ID
*/
String userId() default "create_user";
/**
* 角色 ID角色和部门关联表
*/
String roleId() default "role_id";
/**
* 部门表别名
*/
String deptTableAlias() default "sys_dept";
/**
* 角色和部门关联表别名
*/
String roleDeptTableAlias() default "sys_role_dept";
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.datapermission.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.continew.starter.core.constant.PropertiesConstants;
/**
* 数据权限配置属性
*
* @author Charles7c
* @since 2.7.0
*/
@ConfigurationProperties(PropertiesConstants.DATA_PERMISSION)
public class DataPermissionProperties {
/**
* 是否启用多租户
*/
private boolean enabled = true;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.datapermission.enums;
/**
* 数据权限枚举
*
* @author Charles7c
* @since 1.1.0
*/
public enum DataScope {
/**
* 全部数据权限
*/
ALL,
/**
* 本部门及以下数据权限
*/
DEPT_AND_CHILD,
/**
* 本部门数据权限
*/
DEPT,
/**
* 仅本人数据权限
*/
SELF,
/**
* 自定义数据权限
*/
CUSTOM,
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.datapermission.filter;
import top.continew.starter.extension.datapermission.model.UserContext;
/**
* 数据权限用户上下文提供者
*
* @author Charles7c
* @since 1.1.0
*/
public interface DataPermissionUserContextProvider {
/**
* 是否过滤
*
* @return true过滤false不过滤
*/
boolean isFilter();
/**
* 获取用户上下文
*
* @return 用户上下文
*/
UserContext getUserContext();
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.datapermission.model;
import top.continew.starter.extension.datapermission.enums.DataScope;
/**
* 角色上下文
*
* @author Charles7c
* @since 1.1.0
*/
public class RoleContext {
/**
* 角色 ID
*/
private String roleId;
/**
* 数据权限
*/
private DataScope dataScope;
public RoleContext() {
}
public RoleContext(String roleId, DataScope dataScope) {
this.roleId = roleId;
this.dataScope = dataScope;
}
public String getRoleId() {
return roleId;
}
public void setRoleId(String roleId) {
this.roleId = roleId;
}
public DataScope getDataScope() {
return dataScope;
}
public void setDataScope(DataScope dataScope) {
this.dataScope = dataScope;
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.datapermission.model;
import java.util.Set;
/**
* 用户上下文
*
* @author Charles7c
* @since 1.1.0
*/
public class UserContext {
/**
* 用户 ID
*/
private String userId;
/**
* 角色列表
*/
private Set<RoleContext> roles;
/**
* 部门 ID
*/
private String deptId;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public Set<RoleContext> getRoles() {
return roles;
}
public void setRoles(Set<RoleContext> roles) {
this.roles = roles;
}
public String getDeptId() {
return deptId;
}
public void setDeptId(String deptId) {
this.deptId = deptId;
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter-extension-datapermission</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-extension-datapermission-mp</artifactId>
<description>ContiNew Starter 扩展模块 - 数据权限 - MyBatis Plus ORM 模块</description>
<dependencies>
<!-- MyBatis PlusMyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
</dependency>
<!-- 核心模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-extension-datapermission-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,84 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.datapermission.autoconfigure;
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.ResolvableType;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.extension.datapermission.filter.DataPermissionUserContextProvider;
import top.continew.starter.extension.datapermission.handler.DefaultDataPermissionHandler;
/**
* 数据权限自动配置
*
* @author Charles7c
* @since 2.7.0
*/
@AutoConfiguration
@EnableConfigurationProperties(DataPermissionProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.DATA_PERMISSION, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
public class DataPermissionAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(DataPermissionAutoConfiguration.class);
private DataPermissionAutoConfiguration() {
}
/**
* 数据权限拦截器
*/
@Bean
@ConditionalOnMissingBean
public DataPermissionInterceptor dataPermissionInterceptor(DataPermissionHandler dataPermissionHandler) {
return new DataPermissionInterceptor(dataPermissionHandler);
}
/**
* 数据权限处理器
*/
@Bean
@ConditionalOnMissingBean
public DataPermissionHandler dataPermissionHandler(DataPermissionUserContextProvider dataPermissionUserContextProvider) {
return new DefaultDataPermissionHandler(dataPermissionUserContextProvider);
}
/**
* 数据权限用户上下文提供者
*/
@Bean
@ConditionalOnMissingBean
public DataPermissionUserContextProvider dataPermissionUserContextProvider() {
if (log.isErrorEnabled()) {
log.error("Consider defining a bean of type '{}' in your configuration.", ResolvableType
.forClass(DataPermissionUserContextProvider.class));
}
throw new NoSuchBeanDefinitionException(DataPermissionUserContextProvider.class);
}
static {
log.debug("[ContiNew Starter] - Auto Configuration 'DataPermission' completed initialization.");
}
}

View File

@@ -0,0 +1,238 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.datapermission.handler;
import cn.hutool.core.text.CharSequenceUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.ParenthesedSelect;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.SelectItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.extension.datapermission.annotation.DataPermission;
import top.continew.starter.extension.datapermission.enums.DataScope;
import top.continew.starter.extension.datapermission.filter.DataPermissionUserContextProvider;
import top.continew.starter.extension.datapermission.model.RoleContext;
import top.continew.starter.extension.datapermission.model.UserContext;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Set;
/**
* 默认数据权限处理器
*
* @author <a href="https://gitee.com/baomidou/mybatis-plus/issues/I37I90">DataPermissionInterceptor 如何使用?</a>
* @author Charles7c
* @since 1.1.0
*/
public class DefaultDataPermissionHandler implements DataPermissionHandler {
private static final Logger log = LoggerFactory.getLogger(DefaultDataPermissionHandler.class);
private final DataPermissionUserContextProvider dataPermissionUserContextProvider;
public DefaultDataPermissionHandler(DataPermissionUserContextProvider dataPermissionUserContextProvider) {
this.dataPermissionUserContextProvider = dataPermissionUserContextProvider;
}
@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
try {
Class<?> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId
.lastIndexOf(StringConstants.DOT)));
String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringConstants.DOT) + 1);
Method[] methodArr = clazz.getMethods();
for (Method method : methodArr) {
DataPermission dataPermission = method.getAnnotation(DataPermission.class);
String name = method.getName();
if (null == dataPermission || !CharSequenceUtil.equalsAny(methodName, name, name + "_COUNT")) {
continue;
}
if (dataPermissionUserContextProvider.isFilter()) {
return buildDataScopeFilter(dataPermission, where);
}
}
} catch (ClassNotFoundException e) {
log.error("Data permission handler build data scope filter occurred an error: {}.", e.getMessage(), e);
}
return where;
}
/**
* 构建数据范围过滤条件
*
* @param dataPermission 数据权限
* @param where 当前查询条件
* @return 构建后查询条件
*/
private Expression buildDataScopeFilter(DataPermission dataPermission, Expression where) {
Expression expression = null;
UserContext userContext = dataPermissionUserContextProvider.getUserContext();
Set<RoleContext> roles = userContext.getRoles();
for (RoleContext roleContext : roles) {
DataScope dataScope = roleContext.getDataScope();
if (DataScope.ALL.equals(dataScope)) {
return where;
}
switch (dataScope) {
case DEPT_AND_CHILD -> expression = this
.buildDeptAndChildExpression(dataPermission, userContext, expression);
case DEPT -> expression = this.buildDeptExpression(dataPermission, userContext, expression);
case SELF -> expression = this.buildSelfExpression(dataPermission, userContext, expression);
case CUSTOM -> expression = this.buildCustomExpression(dataPermission, roleContext, expression);
default -> throw new IllegalArgumentException("暂不支持 [%s] 数据权限".formatted(dataScope));
}
}
return null != where ? new AndExpression(where, new Parenthesis(expression)) : expression;
}
/**
* 构建本部门及以下数据权限表达式
*
* <p>
* 处理完后的 SQL 示例:<br /> select t1.* from table as t1 where t1.dept_id in (select id from sys_dept where id =
* xxx or find_in_set(xxx, ancestors));
* </p>
*
* @param dataPermission 数据权限
* @param userContext 用户上下文
* @param expression 处理前的表达式
* @return 处理完后的表达式
*/
private Expression buildDeptAndChildExpression(DataPermission dataPermission,
UserContext userContext,
Expression expression) {
ParenthesedSelect subSelect = new ParenthesedSelect();
PlainSelect select = new PlainSelect();
select.setSelectItems(Collections.singletonList(new SelectItem<>(new Column(dataPermission.id()))));
select.setFromItem(new Table(dataPermission.deptTableAlias()));
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column(dataPermission.id()));
equalsTo.setRightExpression(new LongValue(userContext.getDeptId()));
Function function = new Function();
function.setName("find_in_set");
function.setParameters(new ExpressionList(new LongValue(userContext.getDeptId()), new Column("ancestors")));
select.setWhere(new OrExpression(equalsTo, function));
subSelect.setSelect(select);
// 构建父查询
InExpression inExpression = new InExpression();
inExpression.setLeftExpression(this.buildColumn(dataPermission.tableAlias(), dataPermission.deptId()));
inExpression.setRightExpression(subSelect);
return null != expression ? new OrExpression(expression, inExpression) : inExpression;
}
/**
* 构建本部门数据权限表达式
*
* <p>
* 处理完后的 SQL 示例:<br /> select t1.* from table as t1 where t1.dept_id = xxx;
* </p>
*
* @param dataPermission 数据权限
* @param userContext 用户上下文
* @param expression 处理前的表达式
* @return 处理完后的表达式
*/
private Expression buildDeptExpression(DataPermission dataPermission,
UserContext userContext,
Expression expression) {
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(this.buildColumn(dataPermission.tableAlias(), dataPermission.deptId()));
equalsTo.setRightExpression(new LongValue(userContext.getDeptId()));
return null != expression ? new OrExpression(expression, equalsTo) : equalsTo;
}
/**
* 构建仅本人数据权限表达式
*
* <p>
* 处理完后的 SQL 示例:<br /> select t1.* from table as t1 where t1.create_user = xxx;
* </p>
*
* @param dataPermission 数据权限
* @param userContext 用户上下文
* @param expression 处理前的表达式
* @return 处理完后的表达式
*/
private Expression buildSelfExpression(DataPermission dataPermission,
UserContext userContext,
Expression expression) {
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(this.buildColumn(dataPermission.tableAlias(), dataPermission.userId()));
equalsTo.setRightExpression(new LongValue(userContext.getUserId()));
return null != expression ? new OrExpression(expression, equalsTo) : equalsTo;
}
/**
* 构建自定义数据权限表达式
*
* <p>
* 处理完后的 SQL 示例:<br /> select t1.* from table as t1 where t1.dept_id in (select dept_id from sys_role_dept
* where role_id = xxx);
* </p>
*
* @param dataPermission 数据权限
* @param roleContext 角色上下文
* @param expression 处理前的表达式
* @return 处理完后的表达式
*/
private Expression buildCustomExpression(DataPermission dataPermission,
RoleContext roleContext,
Expression expression) {
ParenthesedSelect subSelect = new ParenthesedSelect();
PlainSelect select = new PlainSelect();
select.setSelectItems(Collections.singletonList(new SelectItem<>(new Column(dataPermission.deptId()))));
select.setFromItem(new Table(dataPermission.roleDeptTableAlias()));
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column(dataPermission.roleId()));
equalsTo.setRightExpression(new LongValue(roleContext.getRoleId()));
select.setWhere(equalsTo);
subSelect.setSelect(select);
// 构建父查询
InExpression inExpression = new InExpression();
inExpression.setLeftExpression(this.buildColumn(dataPermission.tableAlias(), dataPermission.deptId()));
inExpression.setRightExpression(subSelect);
return null != expression ? new OrExpression(expression, inExpression) : inExpression;
}
/**
* 构建 Column
*
* @param tableAlias 表别名
* @param columnName 字段名称
* @return 带表别名字段
*/
private Column buildColumn(String tableAlias, String columnName) {
if (StringUtils.isNotEmpty(tableAlias)) {
return new Column("%s.%s".formatted(tableAlias, columnName));
}
return new Column(columnName);
}
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter-extension</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-extension-datapermission</artifactId>
<packaging>pom</packaging>
<description>ContiNew Starter 扩展模块 - 数据权限</description>
<modules>
<module>continew-starter-extension-datapermission-core</module>
<module>continew-starter-extension-datapermission-mp</module>
</modules>
</project>