本系统实现了三级权限控制:
@PreAuthorize 注解┌─────────────┐ ┌──────────────┐ ┌────────────────┐
│ 客户端 │─────>│ 登录接口 │─────>│ Authentication │
│ │ │ /auth/login │ │ Manager │
└─────────────┘ └──────────────┘ └────────────────┘
↓ ↓
↓ ┌────────────────┐
↓ │ UserDetails │
↓ │ Service │
↓ └────────────────┘
↓ ↓
┌──────────────┐ ↓
│ 生成JWT │ ┌────────────────┐
│ Token │ │ 查询用户 │
└──────────────┘ │ 角色+权限 │
↓ └────────────────┘
↓ ↓
┌──────────────┐ ↓
│ 存储Token到 │<─────────────┘
│ Redis │
└──────────────┘
↓
┌──────────────┐
│ 返回Token给 │
│ 客户端 │
└──────────────┘
本系统采用经典的 RBAC (Role-Based Access Control) 模型:
-- ====================================
-- 用户表
-- ====================================
CREATE TABLE `sys_user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)',
`nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像',
`status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
`create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建人',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` VARCHAR(50) DEFAULT NULL COMMENT '更新人',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '删除标志:0-正常,1-删除',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';
-- ====================================
-- 角色表
-- ====================================
CREATE TABLE `sys_role` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`role_name` VARCHAR(50) NOT NULL COMMENT '角色名称',
`role_key` VARCHAR(50) NOT NULL COMMENT '角色权限字符串',
`role_sort` INT(4) NOT NULL DEFAULT 0 COMMENT '显示顺序',
`status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
`create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建人',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` VARCHAR(50) DEFAULT NULL COMMENT '更新人',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '删除标志:0-正常,1-删除',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_key` (`role_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统角色表';
-- ====================================
-- 菜单/权限表(支持2级菜单 + 按钮权限)
-- ====================================
CREATE TABLE `sys_menu` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`menu_name` VARCHAR(50) NOT NULL COMMENT '菜单名称',
`parent_id` BIGINT(20) NOT NULL DEFAULT 0 COMMENT '父菜单ID,0表示顶级菜单',
`menu_type` CHAR(1) NOT NULL COMMENT '菜单类型:M-目录,C-菜单,F-按钮',
`path` VARCHAR(200) DEFAULT NULL COMMENT '路由地址',
`component` VARCHAR(255) DEFAULT NULL COMMENT '组件路径',
`perms` VARCHAR(100) DEFAULT NULL COMMENT '权限标识(如:system:user:add)',
`icon` VARCHAR(100) DEFAULT NULL COMMENT '菜单图标',
`order_num` INT(4) NOT NULL DEFAULT 0 COMMENT '显示顺序',
`visible` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否可见:0-隐藏,1-显示',
`status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
`create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建人',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` VARCHAR(50) DEFAULT NULL COMMENT '更新人',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '删除标志:0-正常,1-删除',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统菜单表';
-- ====================================
-- 用户-角色关联表
-- ====================================
CREATE TABLE `sys_user_role` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` BIGINT(20) NOT NULL COMMENT '用户ID',
`role_id` BIGINT(20) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_role` (`user_id`, `role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';
-- ====================================
-- 角色-菜单关联表
-- ====================================
CREATE TABLE `sys_role_menu` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`role_id` BIGINT(20) NOT NULL COMMENT '角色ID',
`menu_id` BIGINT(20) NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_menu` (`role_id`, `menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关联表';
-- ====================================
-- 初始化数据
-- ====================================
-- 插入超级管理员用户(密码: admin123,已BCrypt加密)
INSERT INTO `sys_user` (`id`, `username`, `password`, `nickname`, `status`)
VALUES (1, 'admin', '$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZySmDu', '超级管理员', 1);
-- 插入普通用户(密码: user123,已BCrypt加密)
INSERT INTO `sys_user` (`id`, `username`, `password`, `nickname`, `status`)
VALUES (2, 'user', '$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZySmDu', '普通用户', 1);
-- 插入角色
INSERT INTO `sys_role` (`id`, `role_name`, `role_key`, `role_sort`)
VALUES (1, '超级管理员', 'admin', 1);
INSERT INTO `sys_role` (`id`, `role_name`, `role_key`, `role_sort`)
VALUES (2, '普通用户', 'user', 2);
-- 插入一级菜单(目录)
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `icon`, `order_num`, `perms`)
VALUES (1, '系统管理', 0, 'M', '/system', 'system', 1, NULL);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `icon`, `order_num`, `perms`)
VALUES (2, '用户中心', 0, 'M', '/user', 'user', 2, NULL);
-- 插入二级菜单
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `order_num`, `perms`)
VALUES (101, '用户管理', 1, 'C', '/system/user', 'system/user/index', 1, 'system:user:list');
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `order_num`, `perms`)
VALUES (102, '角色管理', 1, 'C', '/system/role', 'system/role/index', 2, 'system:role:list');
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `order_num`, `perms`)
VALUES (103, '菜单管理', 1, 'C', '/system/menu', 'system/menu/index', 3, 'system:menu:list');
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `order_num`, `perms`)
VALUES (201, '个人信息', 2, 'C', '/user/profile', 'user/profile/index', 1, 'user:profile:view');
-- 插入按钮权限(F类型 - 按钮级别权限控制)
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
VALUES (1011, '用户新增', 101, 'F', 1, 'system:user:add');
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
VALUES (1012, '用户修改', 101, 'F', 2, 'system:user:edit');
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
VALUES (1013, '用户删除', 101, 'F', 3, 'system:user:remove');
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
VALUES (1021, '角色新增', 102, 'F', 1, 'system:role:add');
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
VALUES (1022, '角色修改', 102, 'F', 2, 'system:role:edit');
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `order_num`, `perms`)
VALUES (1023, '角色删除', 102, 'F', 3, 'system:role:remove');
-- 用户-角色关联
INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (1, 1);
INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (2, 2);
-- 角色-菜单关联(超级管理员拥有所有权限)
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
VALUES
(1, 1), (1, 2), (1, 101), (1, 102), (1, 103), (1, 201),
(1, 1011), (1, 1012), (1, 1013), (1, 1021), (1, 1022), (1, 1023);
-- 普通用户只有查看权限
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
VALUES (2, 2), (2, 201);
权限标识(perms字段)采用 模块:功能:操作 三段式命名:
| 权限标识 | 说明 | 示例 |
|---|---|---|
system:user:list |
用户列表查看 | 查询用户列表接口 |
system:user:add |
用户新增 | 新增用户按钮 |
system:user:edit |
用户编辑 | 编辑用户按钮 |
system:user:remove |
用户删除 | 删除用户按钮 |
system:user:export |
用户导出 | 导出Excel按钮 |
用户登录流程:
┌──────────────────────────────────────────────────────────────────┐
│ 1. 用户发起登录请求 │
│ POST /auth/login │
│ { username, password } │
└────────────────────────┬─────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 2. Controller接收请求 │
│ SysUserController.login() │
│ 创建 UsernamePasswordAuthenticationToken │
└────────────────────────┬─────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 3. AuthenticationManager 认证 │
│ 调用 authenticate(authenticationToken) │
└────────────────────────┬─────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 4. DaoAuthenticationProvider 处理 │
│ - 调用 UserDetailsService.loadUserByUsername() │
│ - 获取用户详情(包含密码、角色、权限) │
└────────────────────────┬─────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 5. UserDetailsServiceImpl.loadUserByUsername() │
│ ┌──────────────────────────────────────┐ │
│ │ a. 查询用户基本信息 │ │
│ │ getUserWithRolesAndPermissions() │ │
│ └──────────────────┬───────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ b. 查询用户权限列表 │ │
│ │ getPermissionsByUserId() │ │
│ └──────────────────┬───────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ c. 构建 LoginUser 对象 │ │
│ │ 包含:用户信息 + 权限列表 │ │
│ └──────────────────────────────────────┘ │
└────────────────────────┬─────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 6. 密码校验 │
│ PasswordEncoder.matches(rawPassword, encodedPassword) │
│ ✓ 匹配成功 → 继续 │
│ ✗ 匹配失败 → 抛出 BadCredentialsException │
└────────────────────────┬─────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 7. 认证成功,生成Token │
│ ┌──────────────────────────────────────┐ │
│ │ a. 生成 JWT Token │ │
│ │ JwtUtil.generateToken() │ │
│ │ 包含:userId, username, 过期时间 │ │
│ └──────────────────┬───────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ b. 将Token存储到Redis │ │
│ │ key: token:userId │ │
│ │ value: token │ │
│ │ expire: 24小时 │ │
│ └──────────────────────────────────────┘ │
└────────────────────────┬─────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 8. 返回Token给客户端 │
│ { success: true, token: "xxx", userInfo: {...} } │
└──────────────────────────────────────────────────────────────────┘
后续请求认证流程:
┌──────────────────────────────────────────────────────────────────┐
│ 1. 客户端发起业务请求 │
│ Header: Authorization: Bearer {token} │
└────────────────────────┬─────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 2. JwtAuthenticationFilter 拦截 │
│ ┌──────────────────────────────────────┐ │
│ │ a. 从请求头获取Token │ │
│ │ Authorization: Bearer {token} │ │
│ └──────────────────┬───────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ b. 验证Token有效性 │ │
│ │ JwtUtil.validateToken() │ │
│ └──────────────────┬───────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ c. 从Redis获取LoginUser │ │
│ │ TokenService.getLoginUser() │ │
│ └──────────────────┬───────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ d. 将LoginUser设置到SecurityContext │ │
│ │ 包含用户信息和权限列表 │ │
│ └──────────────────────────────────────┘ │
└────────────────────────┬─────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 3. FilterSecurityInterceptor │
│ 检查用户是否有访问当前URL的权限 │
└────────────────────────┬─────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 4. 方法级别权限验证 │
│ @PreAuthorize("hasAuthority('xxx')") │
│ 检查用户是否有执行该方法的权限 │
└────────────────────────┬─────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────────┐
│ 5. 执行业务逻辑,返回结果 │
└──────────────────────────────────────────────────────────────────┘
Spring Security的过滤器链按以下顺序执行:
1. SecurityContextPersistenceFilter - 创建SecurityContext
2. HeaderWriterFilter - 添加安全响应头
3. LogoutFilter - 处理登出请求
4. JwtAuthenticationFilter - JWT认证过滤器(自定义)
5. RequestCacheAwareFilter - 恢复被中断的请求
6. SecurityContextHolderAwareRequestFilter - 包装请求对象
7. AnonymousAuthenticationFilter - 匿名认证
8. SessionManagementFilter - Session管理
9. ExceptionTranslationFilter - 异常转换
10. FilterSecurityInterceptor - 权限验证
作用:Spring Security的核心配置类
关键配置项:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 1. 配置密码编码器(BCrypt)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 2. 暴露认证管理器
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// 3. 配置认证方式
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
// 4. 配置HTTP安全策略
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // 禁用CSRF(使用JWT不需要)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态
.and()
.authorizeRequests()
.antMatchers("/auth/login", "/auth/logout").permitAll() // 公开接口
.anyRequest().authenticated(); // 其他接口需要认证
// 添加JWT过滤器
http.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
}
}
重要说明:
permitAll() vs anonymous():
permitAll():允许所有人访问(包括已认证和未认证用户)anonymous():只允许匿名用户访问(已认证用户会被拒绝)permitAll()作用:加载用户信息、角色和权限
实现要点:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService userService;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
// 1. 查询用户(包含角色信息)
SysUser user = userService.getUserWithRolesAndPermissions(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 2. 查询用户权限列表
List<String> permissions = userService.getPermissionsByUserId(user.getId());
// 3. 构建LoginUser对象(实现UserDetails接口)
return new LoginUser(user, permissions);
}
}
SQL查询示例:
<!-- 查询用户及其角色 -->
<select id="selectUserWithRolesAndPermissions" resultMap="UserWithRolesMap">
SELECT
u.id, u.username, u.password, u.nickname, u.status,
r.id AS role_id, r.role_name, r.role_key
FROM sys_user u
LEFT JOIN sys_user_role ur ON u.id = ur.user_id
LEFT JOIN sys_role r ON ur.role_id = r.id
WHERE u.username = #{username}
AND u.del_flag = 0
AND (r.del_flag = 0 OR r.id IS NULL)
</select>
<!-- 查询用户权限 -->
<select id="selectPermissionsByUserId" resultType="java.lang.String">
SELECT DISTINCT m.perms
FROM sys_user_role ur
LEFT JOIN sys_role_menu rm ON ur.role_id = rm.role_id
LEFT JOIN sys_menu m ON rm.menu_id = m.id
WHERE ur.user_id = #{userId}
AND m.perms IS NOT NULL
AND m.perms != ''
AND m.status = 1
AND m.del_flag = 0
</select>
作用:封装用户详情和权限,实现UserDetails接口
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private SysUser user; // 用户信息
private List<String> permissions; // 权限列表
// 构造方法
public LoginUser(SysUser user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
// 将权限字符串转换为GrantedAuthority
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
// 账户是否启用(检查status字段)
@Override
public boolean isEnabled() {
return user.getStatus() == 1;
}
// 其他方法返回true
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
}
作用:拦截所有请求,验证JWT Token
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
// 1. 从请求头获取token
String token = getTokenFromRequest(request);
// 2. 验证token
if (token != null && jwtUtil.validateToken(token)) {
// 3. 从Redis获取用户信息
LoginUser loginUser = tokenService.getLoginUser(token);
if (loginUser != null) {
// 4. 刷新token有效期
tokenService.verifyToken(loginUser);
// 5. 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
loginUser, null, loginUser.getAuthorities());
// 6. 设置到SecurityContext
SecurityContextHolder.getContext()
.setAuthentication(authentication);
}
}
// 7. 继续过滤器链
chain.doFilter(request, response);
}
// 从请求头提取token
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
作用:生成和解析JWT Token
@Component
public class JwtUtil {
// 密钥(生产环境应从配置文件读取)
private String secret = "your-secret-key-must-be-very-long";
// 过期时间(24小时)
private long expiration = 86400000;
// 生成Token
public String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
// 从Token中获取用户ID
public Long getUserIdFromToken(String token) {
Claims claims = parseToken(token);
return claims.get("userId", Long.class);
}
// 从Token中获取用户名
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims.get("username", String.class);
}
// 验证Token
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
// 解析Token
private Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
}
作用:管理Token的存储和刷新
@Service
public class TokenService {
@Autowired
private RedisCache redisCache;
@Autowired
private JwtUtil jwtUtil;
// Token在Redis中的前缀
private static final String LOGIN_TOKEN_KEY = "login_tokens:";
// 刷新时间(20分钟)
private static final long REFRESH_TIME = 20 * 60 * 1000;
// 过期时间(24小时)
private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000;
/**
* 创建Token
*/
public String createToken(LoginUser loginUser) {
String uuid = UUID.randomUUID().toString();
loginUser.setToken(uuid);
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + EXPIRE_TIME);
// 生成JWT
Map<String, Object> claims = new HashMap<>();
claims.put("userId", loginUser.getUser().getId());
claims.put("username", loginUser.getUsername());
String token = jwtUtil.generateToken(claims);
// 存储到Redis
String tokenKey = getTokenKey(token);
redisCache.setCacheObject(tokenKey, loginUser, EXPIRE_TIME, TimeUnit.MILLISECONDS);
return token;
}
/**
* 获取用户信息
*/
public LoginUser getLoginUser(String token) {
String tokenKey = getTokenKey(token);
return redisCache.getCacheObject(tokenKey);
}
/**
* 刷新Token有效期
*/
public void verifyToken(LoginUser loginUser) {
long currentTime = System.currentTimeMillis();
long expireTime = loginUser.getExpireTime();
// 距离过期时间小于20分钟,自动刷新
if (expireTime - currentTime <= REFRESH_TIME) {
String tokenKey = getTokenKey(loginUser.getToken());
redisCache.expire(tokenKey, EXPIRE_TIME, TimeUnit.MILLISECONDS);
}
}
/**
* 删除Token
*/
public void deleteToken(String token) {
String tokenKey = getTokenKey(token);
redisCache.deleteObject(tokenKey);
}
private String getTokenKey(String token) {
return LOGIN_TOKEN_KEY + token;
}
}
作用:统一处理认证和授权异常
@RestController
@ControllerAdvice
public class ServiceExceptionHandler {
/**
* 处理认证异常(用户名或密码错误)
*/
@ExceptionHandler(AuthenticationException.class)
@ResponseBody
public RestResult<?> handle(AuthenticationException ex) {
ex.printStackTrace();
return RestResult.error("用户名或密码错误");
}
/**
* 处理授权异常(权限不足)
*/
@ExceptionHandler(AccessDeniedException.class)
@ResponseBody
public RestResult<?> handle(AccessDeniedException ex) {
ex.printStackTrace();
return RestResult.error("没有访问权限");
}
/**
* 处理其他异常
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public RestResult<?> handle(Exception ex) {
ex.printStackTrace();
return RestResult.error("请求异常, 请稍后重试!" + ex.getMessage());
}
}
位置:config/SecurityConfig.java
必须实现的方法:
passwordEncoder() - 配置密码编码器authenticationManagerBean() - 暴露认证管理器configure(AuthenticationManagerBuilder) - 配置认证方式configure(HttpSecurity) - 配置HTTP安全策略关键点:
@EnableGlobalMethodSecurity位置:security/UserDetailsServiceImpl.java
必须实现的方法:
loadUserByUsername(String username) - 加载用户信息关键点:
UsernameNotFoundException位置:security/LoginUser.java
必须实现的方法(实现UserDetails接口):
getAuthorities() - 返回权限列表getPassword() - 返回密码getUsername() - 返回用户名isEnabled() - 是否启用(检查status字段)isAccountNonExpired() - 账户是否过期isAccountNonLocked() - 账户是否锁定isCredentialsNonExpired() - 密码是否过期关键点:
GrantedAuthority位置:security/JwtAuthenticationFilter.java
必须实现的方法:
doFilterInternal() - 过滤器逻辑关键点:
位置:utils/JwtUtil.java
必须实现的方法:
generateToken(Map<String, Object> claims) - 生成TokengetUserIdFromToken(String token) - 从Token获取用户IDgetUsernameFromToken(String token) - 从Token获取用户名validateToken(String token) - 验证TokenparseToken(String token) - 解析Token关键点:
位置:security/TokenService.java
必须实现的方法:
createToken(LoginUser loginUser) - 创建Token并存储到RedisgetLoginUser(String token) - 从Redis获取用户信息verifyToken(LoginUser loginUser) - 刷新Token有效期deleteToken(String token) - 删除Token(登出时使用)关键点:
login_tokens:{token}位置:modules/system/controller/SysUserController.java
必须实现的方法:
login(LoginBody loginBody) - 登录接口logout() - 登出接口关键点:
AuthenticationManager.authenticate() 进行认证位置:modules/system/mapper/SysUserMapper.java
必须实现的方法:
// 根据用户名查询用户(包含角色)
SysUser selectUserWithRolesAndPermissions(@Param("username") String username);
// 根据用户ID查询权限列表
List<String> selectPermissionsByUserId(@Param("userId") Long userId);
位置:modules/system/mapper/dao/SysUserMapper.xml
必须实现的SQL:
位置:modules/system/service/SysUserService.java
必须实现的方法:
// 根据用户名查询用户(包含角色和权限)
SysUser getUserWithRolesAndPermissions(String username);
// 根据用户ID查询权限列表
List<String> getPermissionsByUserId(Long userId);
// 根据用户名查询用户(不含角色)
SysUser getUserByUsername(String username);
// 验证密码是否匹配
boolean matchesPassword(String rawPassword, String encodedPassword);
位置:modules/system/entity/SysUser.java
必须包含的字段:
id - 用户IDusername - 用户名password - 密码(BCrypt加密)status - 状态(0-禁用,1-正常)roles - 角色列表(非数据库字段)permissions - 权限列表(非数据库字段)关键点:
password 字段添加 @JsonIgnore 注解,防止返回给前端必须添加的依赖:
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- JAXB API(JDK 11+必需) -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.1</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
必须配置的项:
spring:
redis:
host: 127.0.0.1
port: 6379
password: your-password
database: 0
logging:
level:
org.springframework.security: DEBUG # 开发时开启,便于调试
| 序号 | 组件名称 | 位置 | 类型 | 优先级 |
|---|---|---|---|---|
| 1 | SecurityConfig | config/ | 配置类 | 🔴 必须 |
| 2 | UserDetailsServiceImpl | security/ | 服务类 | 🔴 必须 |
| 3 | LoginUser | security/ | 实体类 | 🔴 必须 |
| 4 | JwtAuthenticationFilter | security/ | 过滤器 | 🔴 必须 |
| 5 | JwtUtil | utils/ | 工具类 | 🔴 必须 |
| 6 | TokenService | security/ | 服务类 | 🔴 必须 |
| 7 | SysUserController | controller/ | 控制器 | 🔴 必须 |
| 8 | SysUserMapper | mapper/ | Mapper接口 | 🔴 必须 |
| 9 | SysUserMapper.xml | mapper/dao/ | MyBatis XML | 🔴 必须 |
| 10 | SysUserService | service/ | 服务接口 | 🔴 必须 |
| 11 | SysUser | entity/ | 实体类 | 🔴 必须 |
| 12 | ServiceExceptionHandler | exception/ | 异常处理 | 🟡 推荐 |
| 13 | RedisCache | utils/ | Redis工具类 | 🟡 推荐 |
@RestController
@RequestMapping("/auth")
public class SysUserController {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private JwtUtil jwtUtil;
@Resource
private SysUserService sysUserService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 用户名密码登录
*/
@PostMapping("/login")
public RestResult<?> login(@RequestBody LoginBody loginBody) {
try {
// 1. 使用Spring Security进行认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginBody.getUsername(),
loginBody.getPassword()
)
);
// 2. 认证成功,设置认证信息
SecurityContextHolder.getContext().setAuthentication(authentication);
// 3. 获取用户信息
SysUser user = sysUserService.getUserByUsername(loginBody.getUsername());
// 4. 生成JWT Token
Map<String, Object> claims = new HashMap<>();
claims.put("username", user.getUsername());
claims.put("userId", user.getId());
String token = jwtUtil.generateToken(claims);
// 5. 将Token存储到Redis(24小时过期)
redisTemplate.opsForValue().set(
"token:" + user.getId(),
token,
24,
TimeUnit.HOURS
);
// 6. 返回结果
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("userInfo", user);
return RestResult.ok("登录成功", result);
} catch (AuthenticationException e) {
// 认证失败
return RestResult.error("用户名或密码错误");
}
}
/**
* 退出登录
*/
@PostMapping("/logout")
public RestResult<?> logout() {
// 1. 获取当前用户
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
if (authentication != null) {
// 2. 删除Redis中的Token
String username = authentication.getName();
SysUser user = sysUserService.getUserByUsername(username);
if (user != null) {
redisTemplate.delete("token:" + user.getId());
}
}
// 3. 清空认证信息
SecurityContextHolder.clearContext();
return RestResult.ok("退出成功");
}
}
@RestController
@RequestMapping("/system/user")
public class UserController {
@Autowired
private SysUserService userService;
/**
* 查询用户列表
* 需要 system:user:list 权限
*/
@GetMapping("/list")
@PreAuthorize("hasAuthority('system:user:list')")
public RestResult<?> list() {
List<SysUser> users = userService.list();
return RestResult.ok(users);
}
/**
* 新增用户
* 需要 system:user:add 权限
*/
@PostMapping("/add")
@PreAuthorize("hasAuthority('system:user:add')")
public RestResult<?> add(@RequestBody SysUser user) {
userService.save(user);
return RestResult.ok("新增成功");
}
/**
* 修改用户
* 需要 system:user:edit 权限
*/
@PutMapping("/edit")
@PreAuthorize("hasAuthority('system:user:edit')")
public RestResult<?> edit(@RequestBody SysUser user) {
userService.updateById(user);
return RestResult.ok("修改成功");
}
/**
* 删除用户
* 需要 system:user:remove 权限
*/
@DeleteMapping("/remove/{id}")
@PreAuthorize("hasAuthority('system:user:remove')")
public RestResult<?> remove(@PathVariable Long id) {
userService.removeById(id);
return RestResult.ok("删除成功");
}
/**
* 组合权限判断
* 需要同时拥有 system:user:list 和 system:user:export 权限
*/
@GetMapping("/export")
@PreAuthorize("hasAuthority('system:user:list') and hasAuthority('system:user:export')")
public RestResult<?> export() {
// 导出Excel逻辑
return RestResult.ok("导出成功");
}
/**
* 角色判断
* 需要admin角色
*/
@GetMapping("/admin-only")
@PreAuthorize("hasRole('admin')")
public RestResult<?> adminOnly() {
return RestResult.ok("管理员专属功能");
}
}
// permission.js - 自定义权限指令
import store from '@/store'
export default {
install(Vue) {
// v-hasPermission 指令
Vue.directive('hasPermission', {
inserted(el, binding, vnode) {
const { value } = binding
const permissions = store.getters.permissions
if (value && value instanceof Array && value.length > 0) {
const hasPermission = permissions.some(permission => {
return value.includes(permission)
})
if (!hasPermission) {
// 没有权限,移除按钮
el.parentNode && el.parentNode.removeChild(el)
}
}
}
})
}
}
<!-- UserList.vue -->
<template>
<div>
<el-table :data="userList">
<el-table-column prop="username" label="用户名"/>
<el-table-column label="操作">
<template slot-scope="scope">
<!-- 新增按钮 - 需要 system:user:add 权限 -->
<el-button
v-hasPermission="['system:user:add']"
@click="handleAdd()">
新增
</el-button>
<!-- 编辑按钮 - 需要 system:user:edit 权限 -->
<el-button
v-hasPermission="['system:user:edit']"
@click="handleEdit(scope.row)">
编辑
</el-button>
<!-- 删除按钮 - 需要 system:user:remove 权限 -->
<el-button
v-hasPermission="['system:user:remove']"
@click="handleDelete(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
// usePermission.js
import { useSelector } from 'react-redux'
export const usePermission = () => {
const permissions = useSelector(state => state.user.permissions)
const hasPermission = (permission) => {
if (Array.isArray(permission)) {
return permission.some(p => permissions.includes(p))
}
return permissions.includes(permission)
}
return { hasPermission }
}
// UserList.jsx
import { usePermission } from '@/hooks/usePermission'
const UserList = () => {
const { hasPermission } = usePermission()
return (
<div>
<Table dataSource={userList}>
<Column title="用户名" dataIndex="username" />
<Column
title="操作"
render={(text, record) => (
<>
{/* 新增按钮 */}
{hasPermission('system:user:add') && (
<Button onClick={() => handleAdd()}>新增</Button>
)}
{/* 编辑按钮 */}
{hasPermission('system:user:edit') && (
<Button onClick={() => handleEdit(record)}>编辑</Button>
)}
{/* 删除按钮 */}
{hasPermission('system:user:remove') && (
<Button onClick={() => handleDelete(record)}>删除</Button>
)}
</>
)}
/>
</Table>
</div>
)
}
# 1. 登录获取Token
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "admin123"
}'
# 响应示例
{
"success": true,
"message": "登录成功",
"code": 200,
"result": {
"token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3MDAwMDAwMDB9.xxx",
"userInfo": {
"id": 1,
"username": "admin",
"nickname": "超级管理员"
}
},
"timestamp": 1700000000000
}
# 2. 使用Token访问受保护的接口
curl -X GET http://localhost:8080/system/user/list \
-H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.xxx"
# 有权限 - 返回数据
{
"success": true,
"code": 200,
"result": [...]
}
# 无权限 - 返回403
{
"success": false,
"message": "没有访问权限",
"code": 403
}
# 测试超级管理员(拥有所有权限)
# 1. 以admin身份登录
TOKEN_ADMIN="eyJhbGciOiJIUzUxMiJ9.xxx"
# 2. 测试新增用户(需要 system:user:add 权限)
curl -X POST http://localhost:8080/system/user/add \
-H "Authorization: Bearer $TOKEN_ADMIN" \
-H "Content-Type: application/json" \
-d '{
"username": "test",
"password": "123456",
"nickname": "测试用户"
}'
# 预期:成功
# 测试普通用户(权限有限)
# 1. 以user身份登录
TOKEN_USER="eyJhbGciOiJIUzUxMiJ9.yyy"
# 2. 测试新增用户(普通用户没有此权限)
curl -X POST http://localhost:8080/system/user/add \
-H "Authorization: Bearer $TOKEN_USER" \
-H "Content-Type: application/json" \
-d '{...}'
# 预期:403 没有访问权限
# 3. 退出登录
curl -X POST http://localhost:8080/auth/logout \
-H "Authorization: Bearer $TOKEN_ADMIN"
# 响应
{
"success": true,
"message": "退出成功",
"code": 200
}
# 4. 退出后再次访问受保护接口
curl -X GET http://localhost:8080/system/user/list \
-H "Authorization: Bearer $TOKEN_ADMIN"
# 预期:401 未认证(因为Token已从Redis中删除)
创建Postman环境变量:
{
"base_url": "http://localhost:8080",
"token": ""
}
测试脚本(自动保存Token):
// 在登录接口的 Tests 标签页添加
if (pm.response.code === 200) {
var jsonData = pm.response.json();
if (jsonData.success && jsonData.result.token) {
pm.environment.set("token", jsonData.result.token);
console.log("Token已保存:", jsonData.result.token);
}
}
原因:
UserDetailsService.loadUserByUsername() 返回的密码为null解决方法:
# 1. 测试密码是否匹配
curl -X POST "http://localhost:8080/auth/test-password" \
-d "username=admin&password=admin123"
# 2. 生成正确的BCrypt密码
curl -X POST "http://localhost:8080/auth/generate-password" \
-d "password=admin123"
# 3. 更新数据库密码
UPDATE sys_user SET password = '$2a$10$xxx' WHERE username = 'admin';
permitAll() 还是 anonymous()?区别:
permitAll():允许所有人访问(已认证 + 未认证)anonymous():只允许匿名用户访问(已认证用户会被拒绝)登录接口必须使用 permitAll(),否则认证过程会失败!
// ✓ 正确
.antMatchers("/auth/login", "/auth/logout").permitAll()
// ✗ 错误(会导致认证失败)
.antMatchers("/auth/login", "/auth/logout").anonymous()
NoClassDefFoundError: javax/xml/bind/DatatypeConverter原因:Java 11移除了 javax.xml.bind 包
解决方法:在 pom.xml 中添加JAXB依赖
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.1</version>
</dependency>
检查项:
# 正确的请求头格式
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.xxx
# 错误示例
Authorization: eyJhbGciOiJIUzUxMiJ9.xxx # 缺少Bearer前缀
Authorization: BearereyJhbGciOiJIUzUxMiJ9.xxx # 缺少空格
原因:未开启方法级权限验证
解决方法:在SecurityConfig上添加注解
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
}
检查步骤:
getPermissionsByUserId() 是否正确返回权限列表getAuthorities() 方法是否正确转换权限// 调试:打印用户权限
@GetMapping("/debug/permissions")
public RestResult<?> getMyPermissions() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) auth.getPrincipal();
return RestResult.ok(loginUser.getPermissions());
}
原因:Redis连接或序列化问题
检查项:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Jackson序列化
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
return template;
}
}
解决方法:在SecurityConfig中配置CORS
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors() // 启用CORS
.and()
.csrf().disable()
// ...
}
// 配置CORS规则
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
// ✗ 错误 String encodedPassword = MD5(rawPassword); // MD5不安全
2. **密码字段添加@JsonIgnore**
```java
@JsonIgnore
private String password;
Token存储位置
Token刷新策略
// 距离过期时间小于20分钟时自动刷新
if (expireTime - currentTime <= 20 * 60 * 1000) {
redisCache.expire(tokenKey, 24 * 60 * 60 * 1000);
}
Token密钥管理
# application.yaml - 生产环境从环境变量读取
jwt:
secret: ${JWT_SECRET:your-default-secret-key}
expiration: 86400000 # 24小时
权限粒度
权限命名规范
模块:功能:操作
system:user:add # 系统管理-用户管理-新增
system:user:edit # 系统管理-用户管理-编辑
report:sales:export # 报表管理-销售报表-导出
数据权限控制
// 根据用户所属部门过滤数据
@DataScope(deptAlias = "d", userAlias = "u")
@PreAuthorize("hasAuthority('system:user:list')")
public List<SysUser> list() {
return userService.selectUserList();
}
@Aspect
@Component
public class LoginLogAspect {
@Around("@annotation(com.xxx.annotation.LoginLog)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long time = System.currentTimeMillis() - startTime;
// 记录登录成功日志
saveLog(joinPoint, time, "SUCCESS");
return result;
} catch (Exception e) {
// 记录登录失败日志
saveLog(joinPoint, 0, "FAIL");
throw e;
}
}
}
使用Redis Pipeline批量操作
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
connection.set("key1".getBytes(), "value1".getBytes());
connection.set("key2".getBytes(), "value2".getBytes());
return null;
});
-- 用户-角色关联索引 CREATE INDEX idx_user_id ON sys_user_role(user_id); CREATE INDEX idx_role_id ON sys_user_role(role_id); ```
使用MyBatis二级缓存
<!-- SysUserMapper.xml -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
```
pacsonline_new/ ├── src/main/java/com/zskk/pacsonline/ │ ├── config/ │ │ ├── SecurityConfig.java # Security配置 │ │ └── RedisConfig.java # Redis配置 │ ├── security/ │ │ ├── UserDetailsServiceImpl.java # 用户详情服务 │ │ ├── LoginUser.java # 用户认证信息 │ │ ├── JwtAuthenticationFilter.java # JWT过滤器 │ │ └── TokenService.java # Token服务 │ ├── utils/ │ │ ├── JwtUtil.java # JWT工具类 │ │ └── RedisCache.java # Redis工具类 │ ├── modules/system/ │ │ ├── controller/ │ │ │ └── SysUserController.java # 用户控制器 │ │ ├── service/ │ │ │ ├── SysUserService.java # 用户服务接口 │ │ │ └── impl/ │ │ │ └── SysUserServiceImpl.java │ │ ├── mapper/ │ │ │ ├── SysUserMapper.java # 用户Mapper接口 │ │ │ └── dao/ │ │ │ └── SysUserMapper.xml # MyBatis XML │ │ └── entity/ │ │ ├── SysUser.java # 用户实体 │ │ ├── SysRole.java # 角色实体 │ │ └── SysMenu.java # 菜单实体 │ └── component/ │ └── exception/ │ └── ServiceExceptionHandler.java # 全局异常处理 ├── src/main/resources/ │ ├── application.yaml # 配置文件 │ └── mapper/ └── doc/
└── security_tables.sql # 数据库表SQL
```
本文档详细介绍了基于Spring Security + JWT的权限认证系统的完整实现方案,涵盖了从数据库设计到代码实现的所有关键环节。
核心要点回顾:
7个核心类必须实现:SecurityConfig、UserDetailsServiceImpl、LoginUser、JwtAuthenticationFilter、JwtUtil、TokenService、登录Controller
RBAC权限模型:用户-角色-权限三级关联
三级权限控制:接口级(URL)、方法级(@PreAuthorize)、按钮级(前端控制)
关键配置:
permitAll() 而非 anonymous()安全最佳实践:
遵循本文档的实现方案,可以快速搭建一个安全、可靠、易扩展的权限认证系统。
文档版本:v1.0 最后更新:2025-10-29 作者:gengjunfang 项目地址:pacsonline_new