档案管理系统权限管理实战: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配置 `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五、数据权限核心实现(MyBatis-Plus 拦截器)

这是实现档案系统数据权限隔离的核心步骤。我们将通过拦截器,在执行 SQL 时自动检查当前用户的数据范围,并改写 SQL。
1. 定义数据范围上下文
使用 ThreadLocal 存储当前登录用户的数据范围信息,确保线程安全。
```java package com.example.archive.config; public class DataScopeContext { private static final ThreadLocal2. 自定义数据权限拦截器
继承 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,极大地降低了开发成本和维护难度。