网站首页/ 信息中心/ 档案百科/

档案管理软件实现细粒度权限控制的硬核实操方案

发布时间:2026年09月04日 16:25:31 浏览量:0

一、数据库表结构设计

实现细粒度权限的核心在于RBAC模型与数据范围的结合。我们需要建立用户、角色、部门以及数据权限规则的关联。以下是基于MySQL的完整建表语句,可直接执行。

1. 基础RBAC表

首先建立用户、角色、部门的基础关联表。

-- 部门表
CREATE TABLE `sys_dept` (
`dept_id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT '0',
`dept_name` varchar(30) DEFAULT '',
PRIMARY KEY (`dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 用户表
CREATE TABLE `sys_user` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT,
`dept_id` bigint(20) DEFAULT NULL,
`user_name` varchar(30) DEFAULT '',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 角色表
CREATE TABLE `sys_role` (
`role_id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_name` varchar(30) DEFAULT '',
`data_scope` char(1) DEFAULT '1' COMMENT '数据范围(1:全部,2:自定义,3:本部门,4:本部门及以下,5:仅本人)',
PRIMARY KEY (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 用户-角色关联表
CREATE TABLE `sys_user_role` (
`user_id` bigint(20) NOT NULL,
`role_id` bigint(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. 档案业务表与数据权限规则表

假设我们有一个档案业务表sys_archive,以及用于角色关联部门的数据权限表sys_role_dept(用于处理“自定义”数据范围)。

-- 档案业务表
CREATE TABLE `sys_archive` (
`archive_id` bigint(20) NOT NULL AUTO_INCREMENT,
`archive_name` varchar(50) DEFAULT '',
`dept_id` bigint(20) DEFAULT NULL COMMENT '所属部门ID',
`user_id` bigint(20) DEFAULT NULL COMMENT '创建人ID',
PRIMARY KEY (`archive_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 角色部门关联表(用于数据范围2:自定义)
CREATE TABLE `sys_role_dept` (
`role_id` bigint(20) NOT NULL,
`dept_id` bigint(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

二、项目依赖与核心注解

本方案基于Spring Boot + MyBatis-Plus实现。首先确保pom.xml中包含必要的依赖。


com.baomidou
mybatis-plus-boot-starter
3.5.3.1


org.springframework.boot
spring-boot-starter-aop

1. 定义数据权限注解

我们需要一个注解来标记在哪些Mapper方法上需要自动拼接SQL权限过滤。

package com.example.annotation;
import java.lang.annotation.;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
/
表别名,SQL拼接时使用
/
String alias() default "";
/
部门字段名
/
String deptColumn() default "dept_id";
/
用户字段名
/
String userColumn() default "user_id";
}

三、核心逻辑实现:AOP切面与SQL拼接

这是实现自动过滤的核心。通过AOP拦截带有@DataScope注解的方法,计算当前用户的数据权限SQL片段,并将其放入ThreadLocal中供MyBatis拦截器使用。

1. 数据权限上下文持有者

package com.example.holder;
public class DataScopeHolder {
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>();
public static void set(String sql) {
THREAD_LOCAL.set(sql);
}
public static String get() {
return THREAD_LOCAL.get();
}
public static void clear() {
THREAD_LOCAL.remove();
}
}

2. 数据权限AOP切面

此切面负责解析用户角色,生成对应的SQL WHERE条件。

package com.example.aspect;
import com.example.annotation.DataScope;
import com.example.holder.DataScopeHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import java.util.List;
@Aspect
@Component
public class DataScopeAspect {
// 假设这是一个获取当前登录用户信息的Service,需根据实际项目替换
// private final SysUserService userService;
@Before("@annotation(controllerDataScope)")
public void doBefore(JoinPoint point, DataScope controllerDataScope) {
// 1. 获取当前用户(此处模拟数据,实际应从SecurityContext获取)
Long userId = getCurrentUserId();
Long deptId = getCurrentDeptId();
List roles = getCurrentRoleIds(); // 获取用户角色ID列表
// 2. 遍历角色,计算最大权限范围(此处简化逻辑,假设用户只有一个主要生效的角色范围)
String dataScope = getDataScopeFromRoles(roles);
String sqlFragment = buildSql(dataScope, deptId, userId, controller.getAlias(), controllerDataScope.deptColumn(), controllerDataScope.userColumn());
// 3. 将SQL片段放入ThreadLocal
DataScopeHolder.set(sqlFragment);
}
private String buildSql(String scope, Long deptId, Long userId, String alias, String deptCol, String userCol) {
StringBuilder sql = new StringBuilder();
String prefix = (alias != null && !"".equals(alias)) ? alias + "." : "";
// "1:全部数据权限"
if ("1".equals(scope)) {
return "";
}
// "2:自定义数据权限" (需查询sys_role_dept表获取关联部门ID)
else if ("2".equals(scope)) {
List deptIds = getCustomDeptIds(); // 实现查询数据库逻辑
sql.append(" AND ").append(prefix).append(deptCol).append(" IN (").append(StringUtils.join(deptIds, ",")).append(")");
}
// "3:本部门数据权限"
else if ("3".equals(scope)) {
sql.append(" AND ").append(prefix).append(deptCol).append(" = ").append(deptId);
}
// "4:本部门及以下数据权限"
else if ("4".equals(scope)) {
List childDeptIds = getChildDeptIds(deptId); // 递归查询子部门
childDeptIds.add(deptId);
sql.append(" AND ").append(prefix).append(deptCol).append(" IN (").append(StringUtils.join(childDeptIds, ",")).append(")");
}
// "5:仅本人数据权限"
else if ("5".equals(scope)) {
sql.append(" AND ").append(prefix).append(userCol).append(" = ").append(userId);
}
return sql.toString();
}
// 模拟方法,实际项目中需注入Service查询数据库
private Long getCurrentUserId() { return 1L; }
private Long getCurrentDeptId() { return 100L; }
private List getCurrentRoleIds() { return List.of(1L); }
private String getDataScopeFromRoles(List roles) { return "3"; } // 模拟:本部门权限
private List getCustomDeptIds() { return List.of(100L, 101L); }
private List getChildDeptIds(Long deptId) { return List.of(101L); }
}

四、MyBatis-Plus拦截器配置

通过实现MyBatis-Plus的InnerInterceptor接口,在SQL执行前读取ThreadLocal中的权限片段并拼接到原SQL中。

1. 实现数据权限拦截器

package com.example.config;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.example.holder.DataScopeHolder;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
public class DataPermissionInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
String sql = boundSql.getSql();
String permissionSql = DataScopeHolder.get();
if (permissionSql != null && !"".equals(permissionSql) && sql.toUpperCase().startsWith("SELECT")) {
try {
// 使用JSqlParser解析SQL
Select select = (Select) CCJSqlParserUtil.parse(sql);
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
// 获取原WHERE条件
Expression where = plainSelect.getWhere();
// 解析权限SQL片段为表达式
Expression permissionExpr = CCJSqlParserUtil.parseExpression(permissionSql.substring(4)); // 去掉开头的 ' AND '
// 拼接条件
if (where != null) {
plainSelect.setWhere(new AndExpression(where, new Parenthesis(permissionExpr)));
} else {
plainSelect.setWhere(permissionExpr);
}
// 重新设置SQL到BoundSql中(注意:这通常需要通过反射或重写插件流程,MP 3.4+ 推荐直接重写 beforeQuery 并修改 boundSql 字段)
// 由于BoundSql的sql字段是final的,MP内部通过MultiBoundSql处理,这里为了演示简单,实际MP插件注册方式如下:
// 在MP配置类中注册此拦截器,MP会自动处理SQL的替换。
// 这里我们修改的是传入的BoundSql对象,但在实际MP源码中,需要直接操作解析后的Statement对象。
// 为了实操落地,我们通常重写 beforeQuery 并不直接修改 boundSql,而是依赖 MP 的机制。
// 但最简单的“落地”方式是直接修改SQL字符串(需注意注入风险,但JSqlParser已处理表达式)。
// 下面是实际修改SQL的反射操作(针对不同版本MP可能略有差异,此处展示通用逻辑):
// 在 MP 3.x 中,通常直接修改 MappedStatement 或使用 MP 提供的 JSqlParserSupport 机制。
// 为了保证代码可运行,我们使用 MP 推荐的 JSqlParserSupport 方式,见下文配置类。
} catch (Exception e) {
e.printStackTrace();
} finally {
DataScopeHolder.clear(); // 清理ThreadLocal
}
}
}
}

2. 优化版拦截器(直接继承JSqlParserSupport)

直接实现InnerInterceptor处理SQL字符串比较繁琐,MyBatis-Plus提供了JSqlParserSupport类来简化操作。

档案管理软件实现细粒度权限控制的硬核实操方案

package com.example.config;
import com.baomidou.mybatisplus.extension.plugins.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.example.holder.DataScopeHolder;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
public class DataPermissionInnerInterceptor extends JsqlParserSupport implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
String permissionSql = DataScopeHolder.get();
if (permissionSql != null && !"".equals(permissionSql)) {
processParser(boundSql.getSql(), (select) -> {
// 处理Select语句
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
Expression where = plainSelect.getWhere();
// 解析生成的权限SQL,去掉开头的 " AND "
Expression permissionExpr = getExpression(permissionSql.substring(4));
if (where != null) {
plainSelect.setWhere(new AndExpression(where, new Parenthesis(permissionExpr)));
} else {
plainSelect.setWhere(permissionExpr);
}
});
}
DataScopeHolder.clear();
}
}

3. 注册拦截器到MyBatis-Plus配置

package com.example.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加数据权限拦截器
interceptor.addInnerInterceptor(new DataPermissionInnerInterceptor());
// 如果需要分页,也在这里添加
// interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

五、业务落地使用

完成上述配置后,在业务代码中只需两步即可实现自动过滤。

1. Mapper接口定义

在Mapper方法上添加@DataScope注解,指定表别名和字段名。

package com.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.annotation.DataScope;
import com.example.entity.SysArchive;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface SysArchiveMapper extends BaseMapper {
/
查询档案列表(自动过滤数据权限)
alias: "a" 对应SQL中的表别名 a
/
@DataScope(alias = "a", deptColumn = "dept_id", userColumn = "user_id")
List selectArchiveList();
}

2. XML编写SQL

在XML中编写SQL时,不要手动添加WHERE条件中的权限部分,只需关注业务逻辑。





六、效果验证

启动项目后,调用SysArchiveMapper.selectArchiveList()方法。

1. 场景一:当前用户角色数据范围为“本部门”(data_scope=3),用户部门ID为100。

控制台打印的实际SQL将变为:

SELECT a.archive_id, a.archive_name, a.dept_id, a.user_id FROM sys_archive a WHERE a.dept_id = 100

2. 场景二:当前用户角色数据范围为“仅本人”(data_scope=5),用户ID为1。

控制台打印的实际SQL将变为:

SELECT a.archive_id, a.archive_name, a.dept_id, a.user_id FROM sys_archive a WHERE a.user_id = 1

通过以上步骤,我们实现了一套基于注解和拦截器的通用细粒度权限方案,新增业务接口时只需添加一个注解即可,无需重复编写过滤代码。

商标档案管理系统:别再让商标资产“睡大觉”了!
商标档案管理系统:别再让商标资产“睡大觉”了!
你有没有发现,很多公司花大价钱注册了一堆商标,结果呢?这些商标就像被扔进了仓库角落的旧文件,没人记得、没人维护,甚至快过期了都无人知晓。这事儿吧,说白了就是管理上的一笔“糊涂账”。
2026年09月04日 16:25:31
土木工程建筑业企业档案培训:实用干货全揭秘
土木工程建筑业企业档案培训:实用干货全揭秘
哎,各位土木届的搬砖人、包工头、资料员们,终于挖到个能救咱们半条命的宝贝了——今天要唠的就是跟咱们每天离不开的“工地隐形生命线”有关的【土木工程建筑业企业档案培训】!我可不是啥自媒体瞎忽悠,我是真踩过...
2026年09月04日 16:25:31
微信咨询
电话联系
QQ客服
微信咨询一对一服务
服务热线: 028-8744 4417
QQ客服: 2305721818