数字档案馆系统RBAC权限模型落地实操指南
一、技术栈选型与项目初始化
数字档案馆系统对数据安全性要求极高,RBAC(Role-Based Access Control)模型是业界标准解决方案。本文将基于Spring Boot 3.2 + Spring Security 6 + JWT + MySQL 8.0构建一套完整的无状态认证授权系统。这种组合能够确保系统在高并发下具备良好的扩展性,同时实现细粒度的权限控制。
我们需要创建一个Spring Boot项目。为了确保零门槛上手,直接使用Maven命令构建基础骨架。打开终端,执行以下命令创建项目目录并生成基础文件:
mkdir digital-archive-auth
cd digital-archive-auth
curl https://start.spring.io/starter.zip -d dependencies=web,security,data-jpa,mysql,validation,lombok -d type=maven-project -d bootVersion=3.2.0 -d baseDir=digital-archive-auth -o starter.zip
unzip starter.zip
rm starter.zip
上述命令将自动引入Web、Security、JPA、MySQL等核心依赖。解压后,使用IDEA或VSCode打开项目即可开始开发。
二、数据库表结构设计与初始化
RBAC模型的核心在于用户、角色、权限三者之间的关系。为了支撑数字档案馆的复杂业务(如档案录入、查阅、审批、归档),我们需要设计五张核心表:sys_user(用户表)、sys_role(角色表)、sys_menu(权限/菜单表)、sys_user_role(用户角色关联表)、sys_role_menu(角色权限关联表)。
请在MySQL客户端执行以下SQL脚本,完成数据库与表的创建,并插入初始化测试数据:
CREATE DATABASE IF NOT EXISTS digital_archive DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE digital_archive;
-- 用户表
CREATE TABLE sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)',
status TINYINT DEFAULT 1 COMMENT '状态 1:启用 0:禁用',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 角色表
CREATE TABLE sys_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_code VARCHAR(50) NOT NULL UNIQUE COMMENT '角色编码',
role_name VARCHAR(50) NOT NULL COMMENT '角色名称'
);
-- 权限菜单表
CREATE TABLE sys_menu (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
menu_url VARCHAR(200) COMMENT '接口路径',
menu_name VARCHAR(50) COMMENT '菜单/权限名称',
parent_id BIGINT DEFAULT 0
);
-- 用户角色关联表
CREATE TABLE sys_user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id)
);
-- 角色权限关联表
CREATE TABLE sys_role_menu (
role_id BIGINT NOT NULL,
menu_id BIGINT NOT NULL,
PRIMARY KEY (role_id, menu_id)
);
-- 初始化数据:密码为 '123456' 的BCrypt加密结果
INSERT INTO sys_user (username, password) VALUES ('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi');
INSERT INTO sys_user (username, password) VALUES ('archivist', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi');
INSERT INTO sys_role (role_code, role_name) VALUES ('ROLE_ADMIN', '系统管理员');
INSERT INTO sys_role (role_code, role_name) VALUES ('ROLE_ARCHIVIST', '档案管理员');
INSERT INTO sys_menu (menu_url, menu_name) VALUES ('/api/files/', '档案文件管理');
INSERT INTO sys_menu (menu_url, menu_name) VALUES ('/api/users/', '用户管理');
-- 关联关系
INSERT INTO sys_user_role VALUES (1, 1); -- admin是管理员
INSERT INTO sys_user_role VALUES (2, 2); -- archivist是档案员
-- 管理员拥有所有权限
INSERT INTO sys_role_menu VALUES (1, 1);
INSERT INTO sys_role_menu VALUES (1, 2);
-- 档案员只有文件管理权限
INSERT INTO sys_role_menu VALUES (2, 1);
三、后端核心代码实现
3.1 配置文件与依赖补充
在pom.xml中追加JWT处理库io.jsonwebtoken:jjwt-api及其实现,版本建议使用0.12.3。接着修改src/main/resources/application.yml,配置数据库连接信息:
spring:
datasource:
url: jdbc:mysql://localhost:3306/digital_archive?useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: your_mysql_password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: none
show-sql: true
jwt:
secret: mySecretKeyForDigitalArchiveSystemMustBeLongEnough
expiration: 86400000
3.2 实体类与Repository层
创建com.archive.entity包,定义User实体类。为了简化代码,这里省略Getter/Setter(使用Lombok@Data):
@Entity(name = "sys_user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private Integer status;
}
创建com.archive.repository包,定义UserRepository接口:
public interface UserRepository extends JpaRepository {
Optional findByUsername(String username);
}
3.3 自定义UserDetailsService实现
这是Spring Security加载用户权限的核心入口。我们需要在查询用户时,一并查询出该用户拥有的所有权限标识。创建com.archive.security.CustomUserDetailsService:
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
private final JdbcTemplate jdbcTemplate;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 查询用户基本信息
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
// 2. 查询用户拥有的权限(通过多表关联查询)
String sql = "SELECT sm.menu_url " +
"FROM sys_user_role sur " +
"JOIN sys_role_menu srm ON sur.role_id = srm.role_id " +
"JOIN sys_menu sm ON srm.menu_id = sm.id " +
"WHERE sur.user_id = ?";
List authorities = jdbcTemplate.queryForList(sql, String.class, user.getId());
// 3. 封装成UserDetails对象返回
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.disabled(user.getStatus() == 0)
.authorities(authorities.toArray(new String[0]))
.build();
}
}
3.4 JWT工具类与认证过滤器
创建JwtUtil用于生成和解析Token。随后创建JwtAuthenticationFilter,该过滤器负责从请求头中提取Token并验证,构建SecurityContext:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
try {
String username = jwtUtil.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(token, userDetails.getUsername())) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (Exception e) {
// Token无效,清除上下文
SecurityContextHolder.clearContext();
}
}
filterChain.doFilter(request, response);
}
}
3.5 Spring Security核心配置
这是整个权限控制的“大脑”。我们需要配置HTTP安全策略,禁用CSRF,配置自定义Filter,并设置不同URL的访问权限。创建SecurityConfig:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable) // 禁用CSRF,前后端分离必须
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 无状态Session
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/login").permitAll() // 放行登录接口
.requestMatchers("/api/users/").hasAuthority("/api/users/") // 需要特定权限
.requestMatchers("/api/files/").hasAuthority("/api/files/") // 需要特定权限
.anyRequest().authenticated() // 其他所有请求都需要认证
)
.addFilterBefore(new JwtAuthenticationFilter(jwtUtil, userDetailsService), UsernamePasswordAuthenticationFilter.class); // 添加JWT过滤器
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3.6 登录接口实现
编写登录接口。用户登录成功后,生成JWT返回给前端。创建AuthController:

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity
四、实操验证与测试
代码编写完毕,启动Spring Boot应用。我们将使用curl命令模拟前端请求,验证权限控制是否生效。
步骤1:使用admin账号登录(拥有所有权限)
执行以下命令获取Token:
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "123456"}'
预期返回包含token的JSON对象。复制返回的Token值(假设为ADMIN_TOKEN)。
步骤2:使用admin访问用户管理接口(预期成功)
curl http://localhost:8080/api/users/list \
-H "Authorization: Bearer ADMIN_TOKEN"
若返回200状态码,说明管理员权限校验通过。
步骤3:使用archivist账号登录(仅拥有档案文件权限)
同样执行登录操作,获取ARCHIVIST_TOKEN。
步骤4:使用archivist访问用户管理接口(预期失败)
curl http://localhost:8080/api/users/list \
-H "Authorization: Bearer ARCHIVIST_TOKEN"
此时系统应返回403 Forbidden错误。因为archivist角色没有/api/users/的权限,这证明了我们的RBAC权限控制已经精确生效。
步骤5:使用archivist访问文件管理接口(预期成功)
curl http://localhost:8080/api/files/upload \
-H "Authorization: Bearer ARCHIVIST_TOKEN"
该请求将成功通过(假设接口存在),验证了档案员可以操作档案文件。
通过以上步骤,我们已经成功构建了一个基于数据库动态权限控制的数字档案馆安全框架。后续开发中,只需在sys_menu表中配置新的接口路径,并在sys_role_menu表中分配给相应角色,即可实现零代码修改的权限扩展。