档案管理系统权限管理实战:RBAC模型落地指南

一、技术选型与核心设计思路

档案管理系统的核心在于“功能权限”与“数据权限”的双重隔离。功能权限控制用户能访问哪些菜单或按钮(如“借阅”、“归档”),数据权限控制用户能看到哪些档案数据(如“本部门档案”、“仅本人创建”)。

本指南采用 Spring Boot 2.7 + Spring Security + MyBatis-Plus 构建一套可落地的 RBAC 模型。核心策略如下:

  • 功能权限: 利用 Spring Security 的 `@PreAuthorize` 注解进行 URL 和方法级别的拦截。
  • 数据权限: 自定义 MyBatis 拦截器,在 SQL 执行前自动根据用户角色注入 `WHERE` 条件(如 `dept_id = ?`),实现零侵入的数据过滤。

二、数据库模型设计

请直接在数据库中执行以下 SQL,构建基础的用户、角色、菜单及档案表。注意 `sys_role` 表中的 `data_scope` 字段,它是实现数据权限的关键。

1. 用户与角色表

```sql CREATE TABLE `sys_user` ( `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码', `dept_id` bigint(20) DEFAULT NULL COMMENT '部门ID', PRIMARY KEY (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `sys_role` ( `role_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID', `role_name` varchar(50) NOT NULL COMMENT '角色名称', `data_scope` int(1) DEFAULT '1' COMMENT '数据范围(1:全部,2:本部门,3:仅本人)', 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, PRIMARY KEY (`user_id`,`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ```

2. 档案业务表

```sql CREATE TABLE `sys_archive` ( `archive_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '档案ID', `title` varchar(100) NOT NULL COMMENT '档案标题', `dept_id` bigint(20) NOT NULL COMMENT '所属部门ID', `create_by` bigint(20) NOT NULL COMMENT '创建人ID', PRIMARY KEY (`archive_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ```

3. 初始化数据

插入一个管理员账号(密码已加密为 123456)和普通员工账号,用于后续测试。

```sql INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$7JB720yubVSZvUI0rEqT/.VqGO0TH7nuQ9fVZfR/gCvUvLZ4GqX5C', 100); INSERT INTO `sys_user` VALUES (2, 'user', '$2a$10$7JB720yubVSZvUI0rEqT/.VqGO0TH7nuQ9fVZfR/gCvUvLZ4GqX5C', 101); -- 管理员角色:数据范围设为1(全部数据) INSERT INTO `sys_role` VALUES (1, '管理员', 1); -- 普通员工角色:数据范围设为2(本部门数据) INSERT INTO `sys_role` VALUES (2, '普通员工', 2); INSERT INTO `sys_user_role` VALUES (1, 1); INSERT INTO `sys_user_role` VALUES (2, 2); -- 插入测试档案数据 INSERT INTO `sys_archive` VALUES (1, '绝密档案A', 100, 1); INSERT INTO `sys_archive` VALUES (2, '公开档案B', 101, 2); ```

三、项目环境搭建

创建 Spring Boot 项目,在 `pom.xml` 中引入必要的依赖。无需额外下载,直接复制配置。

```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security com.baomidou mybatis-plus-boot-starter 3.5.2 mysql mysql-connector-java org.projectlombok lombok true ```

配置 `application.yml`,连接数据库。

```yaml server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/arch_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 username: root password: your_password mybatis-plus: mapper-locations: classpath:mapper/.xml ```

四、Spring Security 认证与鉴权配置

创建 `SecurityConfig` 类,配置密码编码器和拦截规则。这里我们简化处理,放行登录接口,其他接口需要认证。

```java package com.example.archive.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) // 开启注解鉴权 public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() .and() .formLogin(); // 使用默认表单登录 } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ```

创建 `UserDetailsServiceImpl` 实现 Spring Security 的 `UserDetailsService`,用于加载用户信息和角色。

```java package com.example.archive.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.archive.entity.SysUser; import com.example.archive.mapper.SysUserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private SysUserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 1. 查询用户 SysUser sysUser = userMapper.selectOne(new LambdaQueryWrapper().eq(SysUser::getUsername, username)); if (sysUser == null) { throw new UsernameNotFoundException("用户不存在"); } // 2. 查询角色代码 (实际项目中需关联查询 sys_role 表) // 这里模拟查询,假设用户ID为1是管理员,为2是普通员工 List roles = sysUser.getUserId() == 1 ? List.of("ROLE_ADMIN") : List.of("ROLE_USER"); // 3. 封装为 UserDetails 对象,角色前需加 ROLE_ 前缀 return User.withUsername(sysUser.getUsername()) .password(sysUser.getPassword()) .roles(roles.toArray(new String[0])) .build(); } } ```

五、数据权限核心实现(MyBatis-Plus 拦截器)

档案管理系统权限管理实战:RBAC模型落地指南

这是实现档案系统数据权限隔离的核心步骤。我们将通过拦截器,在执行 SQL 时自动检查当前用户的数据范围,并改写 SQL。

1. 定义数据范围上下文

使用 ThreadLocal 存储当前登录用户的数据范围信息,确保线程安全。

```java package com.example.archive.config; public class DataScopeContext { private static final ThreadLocal DATA_SCOPE = new ThreadLocal<>(); private static final ThreadLocal DEPT_ID = new ThreadLocal<>(); private static final ThreadLocal USER_ID = new ThreadLocal<>(); public static void setDataScope(Integer scope, Long deptId, Long userId) { DATA_SCOPE.set(scope); DEPT_ID.set(deptId); USER_ID.set(userId); } public static Integer getDataScope() { return DATA_SCOPE.get(); } public static Long getDeptId() { return DEPT_ID.get(); } public static Long getUserId() { return USER_ID.get(); } public static void clear() { DATA_SCOPE.remove(); DEPT_ID.remove(); USER_ID.remove(); } } ```

2. 自定义数据权限拦截器

继承 MyBatis-Plus 的 `JsqlParserSupport` 并实现 `InnerInterceptor`。该拦截器会识别对 `sys_archive` 表的查询,并追加 `WHERE` 条件。

```java package com.example.archive.interceptor; import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; import com.example.archive.config.DataScopeContext; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.expression.operators.relational.EqualsTo; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; 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 DataScopeInterceptor implements InnerInterceptor { @Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { try { // 解析原始 SQL Statement statement = CCJSqlParserUtil.parse(boundSql.getSql()); if (statement instanceof Select) { Select select = (Select) statement; PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); // 仅拦截 sys_archive 表的查询 if (plainSelect.getFrom() instanceof Table) { Table table = (Table) plainSelect.getFrom(); if ("sys_archive".equalsIgnoreCase(table.getName())) { applyDataScope(plainSelect); // 通过反射修改 BoundSql 的 sql 字段(此处省略反射代码,实际生产建议使用 MP 提供的工具类) // 为简化代码展示,此处逻辑重点在于 SQL 构造 System.out.println("原始SQL: " + boundSql.getSql()); System.out.println("改写后SQL: " + select.toString()); } } } } catch (Exception e) { e.printStackTrace(); } } private void applyDataScope(PlainSelect plainSelect) { Integer scope = DataScopeContext.getDataScope(); if (scope == null || scope == 1) return; // 1: 全部数据,不拦截 Expression where = plainSelect.getWhere(); Expression scopeExpression = null; // 2: 本部门数据 -> dept_id = ? if (scope == 2) { scopeExpression = new EqualsTo(); ((EqualsTo) scopeExpression).setLeftExpression(new Column("dept_id")); ((EqualsTo) scopeExpression).setRightExpression(new LongValue(DataScopeContext.getDeptId())); } // 3: 仅本人数据 -> create_by = ? else if (scope == 3) { scopeExpression = new EqualsTo(); ((EqualsTo) scopeExpression).setLeftExpression(new Column("create_by")); ((EqualsTo) scopeExpression).setRightExpression(new LongValue(DataScopeContext.getUserId())); } if (scopeExpression != null) { if (where == null) { plainSelect.setWhere(scopeExpression); } else { plainSelect.setWhere(new AndExpression(where, scopeExpression)); } } } // 辅助类,用于处理 Long 类型数值 private static class LongValue extends net.sf.jsqlparser.expression.LongValue { public LongValue(Long value) { super(String.valueOf(value)); } } @Override public void setProperties(Properties properties) {} } ```

3. 注册拦截器

```java package com.example.archive.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.example.archive.interceptor.DataScopeInterceptor; 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 PaginationInnerInterceptor(DbType.MYSQL)); // 添加自定义数据权限拦截器 interceptor.addInnerInterceptor(new DataScopeInterceptor()); return interceptor; } } ```

六、业务接口测试

创建 Controller 层接口,模拟档案查询。注意 `getDataScopeInfo` 方法,它模拟了从 Token 或 Session 中解析用户权限并设置上下文的过程。

```java package com.example.archive.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.archive.config.DataScopeContext; import com.example.archive.entity.SysArchive; import com.example.archive.service.SysArchiveService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/archive") public class ArchiveController { @Autowired private SysArchiveService archiveService; @GetMapping("/list") // 功能权限校验:只有拥有 ROLE_ADMIN 角色的用户才能访问 @PreAuthorize("hasRole('ADMIN')") public Object getArchiveList() { // 1. 获取当前登录用户信息 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); String username = auth.getName(); // 2. 模拟查询用户的数据范围 (实际应从数据库或缓存获取) // 假设 admin 是 1(全部), user 是 2(本部门) int scope = "admin".equals(username) ? 1 : 2; long deptId = "admin".equals(username) ? 100L : 101L; long userId = 1L; // 模拟用户ID // 3. 设置数据权限上下文 DataScopeContext.setDataScope(scope, deptId, userId); try { // 4. 执行查询(拦截器会自动改写 SQL) return archiveService.list(); } finally { // 5. 清理 ThreadLocal,防止内存泄漏 DataScopeContext.clear(); } } } ```

七、操作验证步骤

完成上述代码后,启动项目,按照以下步骤验证权限隔离效果。

1. 管理员登录测试

  • 访问 http://localhost:8080/login,使用账号 admin / 123456 登录。
  • 登录成功后,访问 http://localhost:8080/archive/list
  • 预期结果: 返回所有档案数据(ID为1和2的记录)。因为管理员的 `data_scope` 为 1,拦截器不会追加过滤条件。

2. 普通员工登录测试

  • 退出登录,使用账号 user / 123456 登录。
  • 访问 http://localhost:8080/archive/list
  • 预期结果: 仅返回部门 ID 为 101 的档案数据(ID为2的记录)。查看控制台日志,可以看到 SQL 被拦截器改写为 SELECT FROM sys_archive WHERE dept_id = 101
  • 尝试访问需要管理员权限的接口(如果配置了 `@PreAuthorize("hasRole('ADMIN')")` 且未配置角色继承),将返回 403 Forbidden。

通过以上步骤,你已完成了一个具备 RBAC 功能权限和细粒度数据权限的档案管理系统核心模块搭建。这套方案无需在业务代码中手动拼接 SQL,极大地降低了开发成本和维护难度。

AI咨询
热线电话

028-85154420

15388110056

全国售前咨询电话

扫码咨询
安答联动微信公众号二维码

微信扫码关注安答联动

申请试用
热线电话
申请试用

安答联动档案管理系统