# Spring Security + JWT 权限认证完整指南 ## 目录 - [1. 系统架构概述](#1-系统架构概述) - [2. 数据库表设计](#2-数据库表设计) - [3. Spring Security认证流程](#3-spring-security认证流程) - [4. 核心组件详解](#4-核心组件详解) - [5. 开发者实现清单](#5-开发者实现清单) - [6. 代码示例](#6-代码示例) - [7. 测试指南](#7-测试指南) - [8. 常见问题](#8-常见问题) --- ## 1. 系统架构概述 ### 1.1 技术栈 - **Spring Boot 2.2.7** - **Spring Security 5.x** - **JWT (JSON Web Token)** - **Redis** - 存储Token - **MySQL 8.0** - 存储用户、角色、权限数据 - **MyBatis-Plus** - 数据库操作 ### 1.2 权限控制级别 本系统实现了**三级权限控制**: 1. **接口级别** - URL路径访问控制 2. **方法级别** - 使用 `@PreAuthorize` 注解 3. **按钮级别** - 前端根据权限标识显示/隐藏按钮 ### 1.3 认证流程架构图 ``` ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │ 客户端 │─────>│ 登录接口 │─────>│ Authentication │ │ │ │ /auth/login │ │ Manager │ └─────────────┘ └──────────────┘ └────────────────┘ ↓ ↓ ↓ ┌────────────────┐ ↓ │ UserDetails │ ↓ │ Service │ ↓ └────────────────┘ ↓ ↓ ┌──────────────┐ ↓ │ 生成JWT │ ┌────────────────┐ │ Token │ │ 查询用户 │ └──────────────┘ │ 角色+权限 │ ↓ └────────────────┘ ↓ ↓ ┌──────────────┐ ↓ │ 存储Token到 │<─────────────┘ │ Redis │ └──────────────┘ ↓ ┌──────────────┐ │ 返回Token给 │ │ 客户端 │ └──────────────┘ ``` --- ## 2. 数据库表设计 ### 2.1 RBAC权限模型 本系统采用经典的 **RBAC (Role-Based Access Control)** 模型: - 用户表 (sys_user) - 角色表 (sys_role) - 菜单/权限表 (sys_menu) - 用户-角色关联表 (sys_user_role) - 角色-菜单关联表 (sys_role_menu) ### 2.2 完整SQL脚本 ```sql -- ==================================== -- 用户表 -- ==================================== 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); ``` ### 2.3 权限标识命名规范 权限标识(perms字段)采用 **模块:功能:操作** 三段式命名: | 权限标识 | 说明 | 示例 | |---------|------|------| | `system:user:list` | 用户列表查看 | 查询用户列表接口 | | `system:user:add` | 用户新增 | 新增用户按钮 | | `system:user:edit` | 用户编辑 | 编辑用户按钮 | | `system:user:remove` | 用户删除 | 删除用户按钮 | | `system:user:export` | 用户导出 | 导出Excel按钮 | --- ## 3. Spring Security认证流程 ### 3.1 完整认证流程图 ``` 用户登录流程: ┌──────────────────────────────────────────────────────────────────┐ │ 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. 执行业务逻辑,返回结果 │ └──────────────────────────────────────────────────────────────────┘ ``` ### 3.2 核心过滤器链顺序 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 - 权限验证 ``` --- ## 4. 核心组件详解 ### 4.1 SecurityConfig(安全配置类) **作用**:Spring Security的核心配置类 **关键配置项**: ```java @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()`** ### 4.2 UserDetailsServiceImpl(用户详情服务) **作用**:加载用户信息、角色和权限 **实现要点**: ```java @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 permissions = userService.getPermissionsByUserId(user.getId()); // 3. 构建LoginUser对象(实现UserDetails接口) return new LoginUser(user, permissions); } } ``` **SQL查询示例**: ```xml ``` ### 4.3 LoginUser(用户认证信息) **作用**:封装用户详情和权限,实现UserDetails接口 ```java @Data @NoArgsConstructor public class LoginUser implements UserDetails { private SysUser user; // 用户信息 private List permissions; // 权限列表 // 构造方法 public LoginUser(SysUser user, List permissions) { this.user = user; this.permissions = permissions; } // 将权限字符串转换为GrantedAuthority @Override public Collection 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; } } ``` ### 4.4 JwtAuthenticationFilter(JWT认证过滤器) **作用**:拦截所有请求,验证JWT Token ```java @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; } } ``` ### 4.5 JwtUtil(JWT工具类) **作用**:生成和解析JWT Token ```java @Component public class JwtUtil { // 密钥(生产环境应从配置文件读取) private String secret = "your-secret-key-must-be-very-long"; // 过期时间(24小时) private long expiration = 86400000; // 生成Token public String generateToken(Map 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(); } } ``` ### 4.6 TokenService(Token服务) **作用**:管理Token的存储和刷新 ```java @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 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; } } ``` ### 4.7 全局异常处理器 **作用**:统一处理认证和授权异常 ```java @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()); } } ``` --- ## 5. 开发者实现清单 ### 5.1 必须实现的组件(7个核心类) #### ✅ 1. SecurityConfig(安全配置类) **位置**:`config/SecurityConfig.java` **必须实现的方法**: - `passwordEncoder()` - 配置密码编码器 - `authenticationManagerBean()` - 暴露认证管理器 - `configure(AuthenticationManagerBuilder)` - 配置认证方式 - `configure(HttpSecurity)` - 配置HTTP安全策略 **关键点**: - 禁用CSRF和Session - 配置公开接口(登录、登出等) - 添加JWT过滤器 - 启用方法级权限注解 `@EnableGlobalMethodSecurity` --- #### ✅ 2. UserDetailsServiceImpl(用户详情服务) **位置**:`security/UserDetailsServiceImpl.java` **必须实现的方法**: - `loadUserByUsername(String username)` - 加载用户信息 **关键点**: - 查询用户基本信息(包含密码) - 查询用户权限列表 - 返回LoginUser对象 - 用户不存在时抛出 `UsernameNotFoundException` --- #### ✅ 3. LoginUser(用户认证信息) **位置**:`security/LoginUser.java` **必须实现的方法**(实现UserDetails接口): - `getAuthorities()` - 返回权限列表 - `getPassword()` - 返回密码 - `getUsername()` - 返回用户名 - `isEnabled()` - 是否启用(检查status字段) - `isAccountNonExpired()` - 账户是否过期 - `isAccountNonLocked()` - 账户是否锁定 - `isCredentialsNonExpired()` - 密码是否过期 **关键点**: - 封装用户信息和权限列表 - 将权限字符串转换为 `GrantedAuthority` --- #### ✅ 4. JwtAuthenticationFilter(JWT认证过滤器) **位置**:`security/JwtAuthenticationFilter.java` **必须实现的方法**: - `doFilterInternal()` - 过滤器逻辑 **关键点**: - 从请求头提取Token - 验证Token有效性 - 从Redis获取用户信息 - 设置认证信息到SecurityContext - 继续过滤器链 --- #### ✅ 5. JwtUtil(JWT工具类) **位置**:`utils/JwtUtil.java` **必须实现的方法**: - `generateToken(Map claims)` - 生成Token - `getUserIdFromToken(String token)` - 从Token获取用户ID - `getUsernameFromToken(String token)` - 从Token获取用户名 - `validateToken(String token)` - 验证Token - `parseToken(String token)` - 解析Token **关键点**: - 使用HMAC-SHA512算法签名 - 设置过期时间 - 异常处理(ExpiredJwtException等) --- #### ✅ 6. TokenService(Token服务) **位置**:`security/TokenService.java` **必须实现的方法**: - `createToken(LoginUser loginUser)` - 创建Token并存储到Redis - `getLoginUser(String token)` - 从Redis获取用户信息 - `verifyToken(LoginUser loginUser)` - 刷新Token有效期 - `deleteToken(String token)` - 删除Token(登出时使用) **关键点**: - Token存储在Redis中,key为 `login_tokens:{token}` - 自动刷新机制:距离过期时间小于20分钟时自动续期 - 默认过期时间24小时 --- #### ✅ 7. 登录/登出Controller **位置**:`modules/system/controller/SysUserController.java` **必须实现的方法**: - `login(LoginBody loginBody)` - 登录接口 - `logout()` - 登出接口 **关键点**: - 使用 `AuthenticationManager.authenticate()` 进行认证 - 认证成功后生成Token - 将Token存储到Redis - 返回Token和用户信息给客户端 - 登出时删除Redis中的Token --- ### 5.2 数据访问层(Mapper) #### ✅ 8. SysUserMapper **位置**:`modules/system/mapper/SysUserMapper.java` **必须实现的方法**: ```java // 根据用户名查询用户(包含角色) SysUser selectUserWithRolesAndPermissions(@Param("username") String username); // 根据用户ID查询权限列表 List selectPermissionsByUserId(@Param("userId") Long userId); ``` #### ✅ 9. SysUserMapper.xml **位置**:`modules/system/mapper/dao/SysUserMapper.xml` **必须实现的SQL**: - 用户+角色关联查询 - 用户权限查询(用户→角色→菜单) --- ### 5.3 服务层(Service) #### ✅ 10. SysUserService **位置**:`modules/system/service/SysUserService.java` **必须实现的方法**: ```java // 根据用户名查询用户(包含角色和权限) SysUser getUserWithRolesAndPermissions(String username); // 根据用户ID查询权限列表 List getPermissionsByUserId(Long userId); // 根据用户名查询用户(不含角色) SysUser getUserByUsername(String username); // 验证密码是否匹配 boolean matchesPassword(String rawPassword, String encodedPassword); ``` --- ### 5.4 实体类(Entity) #### ✅ 11. SysUser **位置**:`modules/system/entity/SysUser.java` **必须包含的字段**: - `id` - 用户ID - `username` - 用户名 - `password` - 密码(BCrypt加密) - `status` - 状态(0-禁用,1-正常) - `roles` - 角色列表(非数据库字段) - `permissions` - 权限列表(非数据库字段) **关键点**: - `password` 字段添加 `@JsonIgnore` 注解,防止返回给前端 --- ### 5.5 配置文件 #### ✅ 12. pom.xml **必须添加的依赖**: ```xml org.springframework.boot spring-boot-starter-security io.jsonwebtoken jjwt 0.9.1 javax.xml.bind jaxb-api 2.3.1 org.glassfish.jaxb jaxb-runtime 2.3.1 org.springframework.boot spring-boot-starter-data-redis ``` #### ✅ 13. application.yaml **必须配置的项**: ```yaml spring: redis: host: 127.0.0.1 port: 6379 password: your-password database: 0 logging: level: org.springframework.security: DEBUG # 开发时开启,便于调试 ``` --- ### 5.6 完整实现清单总结 | 序号 | 组件名称 | 位置 | 类型 | 优先级 | |-----|---------|------|------|-------| | 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工具类 | 🟡 推荐 | --- ## 6. 代码示例 ### 6.1 登录接口完整实现 ```java @RestController @RequestMapping("/auth") public class SysUserController { @Resource private AuthenticationManager authenticationManager; @Resource private JwtUtil jwtUtil; @Resource private SysUserService sysUserService; @Autowired private RedisTemplate 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 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 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("退出成功"); } } ``` ### 6.2 方法级权限控制 ```java @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 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("管理员专属功能"); } } ``` ### 6.3 前端按钮级权限控制 #### Vue示例(使用自定义指令) ```javascript // 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) } } } }) } } ``` ```vue ``` #### React示例(使用Hook) ```javascript // 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 (
( <> {/* 新增按钮 */} {hasPermission('system:user:add') && ( )} {/* 编辑按钮 */} {hasPermission('system:user:edit') && ( )} {/* 删除按钮 */} {hasPermission('system:user:remove') && ( )} )} />
) } ``` --- ## 7. 测试指南 ### 7.1 登录测试 ```bash # 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 } ``` ### 7.2 携带Token访问受保护接口 ```bash # 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 } ``` ### 7.3 测试不同权限级别 ```bash # 测试超级管理员(拥有所有权限) # 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 没有访问权限 ``` ### 7.4 退出登录测试 ```bash # 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中删除) ``` ### 7.5 Postman测试集合 创建Postman环境变量: ```json { "base_url": "http://localhost:8080", "token": "" } ``` 测试脚本(自动保存Token): ```javascript // 在登录接口的 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); } } ``` --- ## 8. 常见问题 ### 8.1 认证问题 #### Q1: 登录时提示"Bad credentials",但密码确实正确? **原因**: 1. 数据库中的密码不是正确的BCrypt加密值 2. `UserDetailsService.loadUserByUsername()` 返回的密码为null 3. 密码编码器配置不正确 **解决方法**: ```bash # 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'; ``` #### Q2: SecurityConfig中应该使用 `permitAll()` 还是 `anonymous()`? **区别**: - `permitAll()`:允许所有人访问(已认证 + 未认证) - `anonymous()`:只允许匿名用户访问(已认证用户会被拒绝) **登录接口必须使用 `permitAll()`**,否则认证过程会失败! ```java // ✓ 正确 .antMatchers("/auth/login", "/auth/logout").permitAll() // ✗ 错误(会导致认证失败) .antMatchers("/auth/login", "/auth/logout").anonymous() ``` ### 8.2 JWT问题 #### Q3: JDK 11运行时报错:`NoClassDefFoundError: javax/xml/bind/DatatypeConverter` **原因**:Java 11移除了 `javax.xml.bind` 包 **解决方法**:在 `pom.xml` 中添加JAXB依赖 ```xml javax.xml.bind jaxb-api 2.3.1 org.glassfish.jaxb jaxb-runtime 2.3.1 ``` #### Q4: Token验证失败,但Token确实有效? **检查项**: 1. JWT密钥是否一致(secret) 2. Token是否过期 3. Redis中是否存在该Token 4. Token格式是否正确(Bearer + 空格 + token) ```bash # 正确的请求头格式 Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.xxx # 错误示例 Authorization: eyJhbGciOiJIUzUxMiJ9.xxx # 缺少Bearer前缀 Authorization: BearereyJhbGciOiJIUzUxMiJ9.xxx # 缺少空格 ``` ### 8.3 权限问题 #### Q5: @PreAuthorize 注解不生效? **原因**:未开启方法级权限验证 **解决方法**:在SecurityConfig上添加注解 ```java @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { // ... } ``` #### Q6: 用户明明有权限,但还是返回403? **检查步骤**: 1. 确认权限标识是否正确(区分大小写) 2. 确认数据库中用户-角色-权限关联是否正确 3. 确认 `getPermissionsByUserId()` 是否正确返回权限列表 4. 确认LoginUser的 `getAuthorities()` 方法是否正确转换权限 ```java // 调试:打印用户权限 @GetMapping("/debug/permissions") public RestResult getMyPermissions() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) auth.getPrincipal(); return RestResult.ok(loginUser.getPermissions()); } ``` ### 8.4 Redis问题 #### Q7: Token生成成功,但后续请求获取不到用户信息? **原因**:Redis连接或序列化问题 **检查项**: 1. Redis服务是否正常运行 2. Redis连接配置是否正确 3. RedisTemplate序列化配置 ```java @Configuration public class RedisConfig { @Bean public RedisTemplate redisTemplate( RedisConnectionFactory factory) { RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 使用Jackson序列化 Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); return template; } } ``` ### 8.5 跨域问题 #### Q8: 前后端分离时,登录接口报跨域错误? **解决方法**:在SecurityConfig中配置CORS ```java @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; } ``` --- ## 9. 最佳实践 ### 9.1 密码安全 1. **永远使用BCrypt加密密码** ```java // ✓ 正确 String encodedPassword = passwordEncoder.encode(rawPassword); // ✗ 错误 String encodedPassword = MD5(rawPassword); // MD5不安全 ``` 2. **密码字段添加@JsonIgnore** ```java @JsonIgnore private String password; ``` 3. **密码复杂度要求** - 最小长度8位 - 包含大小写字母、数字、特殊字符 - 定期更换密码 ### 9.2 Token安全 1. **Token存储位置** - ✓ 前端:localStorage 或 sessionStorage - ✗ 前端:Cookie(容易被CSRF攻击) - ✓ 后端:Redis(可快速失效) 2. **Token刷新策略** ```java // 距离过期时间小于20分钟时自动刷新 if (expireTime - currentTime <= 20 * 60 * 1000) { redisCache.expire(tokenKey, 24 * 60 * 60 * 1000); } ``` 3. **Token密钥管理** ```yaml # application.yaml - 生产环境从环境变量读取 jwt: secret: ${JWT_SECRET:your-default-secret-key} expiration: 86400000 # 24小时 ``` ### 9.3 权限设计 1. **权限粒度** - 粗粒度:模块级(system:user) - 中粒度:功能级(system:user:list) - 细粒度:按钮级(system:user:add) 2. **权限命名规范** ``` 模块:功能:操作 system:user:add # 系统管理-用户管理-新增 system:user:edit # 系统管理-用户管理-编辑 report:sales:export # 报表管理-销售报表-导出 ``` 3. **数据权限控制** ```java // 根据用户所属部门过滤数据 @DataScope(deptAlias = "d", userAlias = "u") @PreAuthorize("hasAuthority('system:user:list')") public List list() { return userService.selectUserList(); } ``` ### 9.4 日志记录 ```java @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; } } } ``` --- ## 10. 性能优化 ### 10.1 Redis优化 1. **使用Redis Pipeline批量操作** ```java redisTemplate.executePipelined((RedisCallback) connection -> { connection.set("key1".getBytes(), "value1".getBytes()); connection.set("key2".getBytes(), "value2".getBytes()); return null; }); ``` 2. **合理设置过期时间** - Token:24小时 - 验证码:5分钟 - 短期缓存:1小时 ### 10.2 数据库优化 1. **为常用查询添加索引** ```sql -- 用户名索引 CREATE UNIQUE INDEX idx_username ON sys_user(username); -- 用户-角色关联索引 CREATE INDEX idx_user_id ON sys_user_role(user_id); CREATE INDEX idx_role_id ON sys_user_role(role_id); ``` 2. **使用MyBatis二级缓存** ```xml ``` --- ## 11. 附录 ### 11.1 完整项目结构 ``` 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 ``` ### 11.2 参考资料 - [Spring Security官方文档](https://docs.spring.io/spring-security/reference/) - [JWT官方网站](https://jwt.io/) - [BCrypt密码加密](https://www.baeldung.com/spring-security-registration-password-encoding-bcrypt) - [Spring Security架构](https://spring.io/guides/topicals/spring-security-architecture/) --- ## 12. 总结 本文档详细介绍了基于Spring Security + JWT的权限认证系统的完整实现方案,涵盖了从数据库设计到代码实现的所有关键环节。 **核心要点回顾**: 1. **7个核心类必须实现**:SecurityConfig、UserDetailsServiceImpl、LoginUser、JwtAuthenticationFilter、JwtUtil、TokenService、登录Controller 2. **RBAC权限模型**:用户-角色-权限三级关联 3. **三级权限控制**:接口级(URL)、方法级(@PreAuthorize)、按钮级(前端控制) 4. **关键配置**: - 登录接口使用 `permitAll()` 而非 `anonymous()` - JDK 11+ 需要添加JAXB依赖 - 密码必须使用BCrypt加密 - Token存储在Redis中,支持自动刷新 5. **安全最佳实践**: - 密码加@JsonIgnore注解 - Token密钥从环境变量读取 - 记录登录日志 - 合理设置Token过期时间 遵循本文档的实现方案,可以快速搭建一个安全、可靠、易扩展的权限认证系统。 --- **文档版本**:v1.0 **最后更新**:2025-10-29 **作者**:gengjunfang **项目地址**:pacsonline_new