# claude-code-spring-security **Repository Path**: zdp666/claude-code-spring-security ## Basic Information - **Project Name**: claude-code-spring-security - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-08 - **Last Updated**: 2026-05-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Spring Security 权限校验系统 基于 **Spring Boot 3.2 + Spring Security + JWT + MyBatis-Plus** 的权限校验演示项目,核心代码从 RuoYi-Vue 框架迁移而来。 > **快速开始**: 配置好 MySQL 后运行 `mvn spring-boot:run`,访问 `http://localhost:8080/login.html` --- ## 目录 1. [项目架构概览](#1-项目架构概览) 2. [核心功能清单](#2-核心功能清单) 3. [Spring Security 配置详解](#3-spring-security-配置详解) 4. [认证流程 (Authentication)](#4-认证流程-authentication) 5. [JWT Token 机制](#5-jwt-token-机制) 6. [授权与权限校验 (Authorization)](#6-授权与权限校验-authorization) 7. [RBAC 数据模型](#7-rbac-数据模型) 8. [前端权限集成](#8-前端权限集成) 9. [快速启动指南](#9-快速启动指南) 10. [Spring Security 最佳实践与进阶](#10-spring-security-最佳实践与进阶) --- ## 1. 项目架构概览 ### 1.1 技术栈 | 层级 | 技术 | 版本 | |------|------|------| | 框架 | Spring Boot | 3.2.5 | | 安全 | Spring Security | 6.x (随 Boot) | | 认证 | JWT (jjwt) | 0.12.5 | | ORM | MyBatis-Plus | 3.5.9 | | 数据库 | MySQL | 8.0 | | 密码加密 | BCrypt | Spring Security 内置 | | 前端 | 原生 HTML/JS | — | ### 1.2 项目结构 ``` src/main/java/com/demo/security/ ├── SecurityApplication.java # 启动类 ├── annotation/ │ └── Anonymous.java # 匿名访问注解(标记无需认证的接口) ├── config/ │ ├── SecurityConfig.java # Spring Security 核心配置 │ ├── PermitAllUrlProperties.java # 自动收集 @Anonymous 注解的 URL │ └── DataInitializer.java # 测试数据初始化器 ├── constant/ # 常量定义 │ ├── Constants.java # 通用常量(Token前缀、权限通配符等) │ ├── CacheConstants.java # 缓存Key前缀 │ ├── HttpStatus.java # HTTP状态码常量 │ └── UserConstants.java # 用户/角色/菜单常量 ├── controller/ │ ├── SysLoginController.java # 登录、用户信息、路由接口 │ └── TestController.java # 权限测试沙箱(教学用) ├── domain/ # 实体类 │ ├── BaseEntity.java # 实体基类(审计字段) │ ├── SysUser.java # 用户实体 │ ├── SysRole.java # 角色实体 │ ├── SysMenu.java # 菜单实体 │ ├── LoginUser.java # 登录用户封装(实现UserDetails) │ ├── LoginBody.java # 登录请求DTO │ └── AjaxResult.java # 统一响应格式 ├── enums/ │ └── UserStatus.java # 用户状态枚举 ├── exception/ # 异常体系 │ ├── ServiceException.java # 业务异常基类 │ └── user/ # 用户相关异常(登录、验证码等) ├── mapper/ # MyBatis Mapper接口 │ ├── SysUserMapper.java # 继承BaseMapper,XML实现联表查询 │ ├── SysRoleMapper.java │ └── SysMenuMapper.java ├── security/ │ ├── context/ # Security上下文持有者 │ │ ├── AuthenticationContextHolder.java │ │ └── PermissionContextHolder.java │ ├── filter/ │ │ └── JwtAuthenticationTokenFilter.java # JWT认证过滤器(核心) │ └── handle/ │ ├── AuthenticationEntryPointImpl.java # 认证失败处理器 │ └── LogoutSuccessHandlerImpl.java # 登出成功处理器 ├── service/ │ ├── UserDetailsServiceImpl.java # Spring Security认证入口 │ ├── SysLoginService.java # 登录流程编排 │ ├── SysPermissionService.java # 权限计算(用户→角色→菜单→权限标识) │ ├── PermissionService.java # @PreAuthorize 注解方法载体 │ ├── TokenService.java # JWT Token生命周期管理 │ └── TokenCacheService.java # Token本地缓存(免Redis) └── utils/ ├── SecurityUtils.java # Security工具类 ├── StringUtils.java # 字符串工具类 └── uuid/IdUtils.java # UUID生成器 src/main/resources/ ├── application.yml # 主配置 ├── mapper/ # MyBatis XML SQL映射 │ ├── SysUserMapper.xml │ ├── SysRoleMapper.xml │ └── SysMenuMapper.xml └── static/ # 前端静态页面 ├── login.html # 登录页 └── index.html # 控制台(权限测试) ``` --- ## 2. 核心功能清单 | 功能 | 说明 | |------|------| | **JWT 无状态认证** | 登录后签发 JWT,每次请求通过 Authorization Header 携带 | | **BCrypt 密码加密** | 密码使用 BCrypt 哈希存储,不可逆,彩虹表攻击无效 | | **RBAC 权限模型** | 用户 → 角色 → 菜单 → 权限标识,四层关联 | | **细粒度鉴权** | `@PreAuthorize("@ss.hasPermi('system:user:list')")` 精确到按钮级别 | | **角色鉴权** | `@PreAuthorize("@ss.hasRole('admin')")` 粗粒度角色判断 | | **匿名访问标记** | `@Anonymous` 注解自动收集,SecurityConfig 批量放行 | | **Token 缓存层** | 本地 ConcurrentHashMap 存储用户信息,支持主动失效 | | **滑动过期** | 用户活跃时自动续期 Token,距离过期 20 分钟内触发 | | **软删除** | 用户通过 `del_flag` 标记删除,数据不物理删除 | | **登录审计** | 记录登录 IP、时间、User-Agent | --- ## 3. Spring Security 配置详解 ### 3.1 SecurityConfig — 核心配置类 > **文件位置**: `config/SecurityConfig.java` ```java @EnableMethodSecurity(prePostEnabled = true, securedEnabled = true) // 开启方法级权限注解 @Configuration public class SecurityConfig { @Bean protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity // 1. 禁用 CSRF(前后端分离 + JWT 不需要) .csrf(csrf -> csrf.disable()) // 2. 禁用缓存控制,允许 iframe 同源嵌入 .headers(headers -> headers .cacheControl(cache -> cache.disable()) .frameOptions(frame -> frame.sameOrigin())) // 3. 认证异常处理器 .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) // 4. 无状态会话(不使用 HttpSession) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 5. URL 级别的权限规则 .authorizeHttpRequests(requests -> { // 自动收集的 @Anonymous 注解 URL 全部放行 permitAllUrl.getUrls().forEach(url -> requests.requestMatchers(url).permitAll()); // 显式放行登录和静态资源 requests.requestMatchers("/login", "/captchaImage").permitAll() .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js").permitAll() // 其余所有请求需要认证 .anyRequest().authenticated(); }) // 6. 登出配置 .logout(logout -> logout.logoutUrl("/logout") .logoutSuccessHandler(logoutSuccessHandler)) // 7. 在 UsernamePasswordAuthenticationFilter 之前插入 JWT 过滤器 .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) .build(); } // 密码编码器 Bean @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } } ``` #### 关键配置解读 **a) `@EnableMethodSecurity(prePostEnabled = true)`** 启用 `@PreAuthorize` / `@PostAuthorize` 注解支持。Spring Security 通过 AOP 代理拦截带有这些注解的方法,在执行前(或后)做权限判断。 **b) `SessionCreationPolicy.STATELESS`** 设置为"无状态",Spring Security 不会创建 `HttpSession`,也不会从 Session 中获取 SecurityContext。每次请求必须通过我们的 JWT 过滤器来还原用户身份。 **c) `.authorizeHttpRequests()` vs 旧版 `.antMatchers()`** Spring Security 6.x 推荐使用 `authorizeHttpRequests`(lambda 风格),旧版 `antMatchers` 已废弃。两者功能等价,新写法类型更安全。 **d) 过滤链顺序很重要** `addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)` 确保 JWT 过滤器在表单登录过滤器之前执行——这样 JWT 请求不会触发表单登录流程。 ### 3.2 PermitAllUrlProperties — 自动收集匿名 URL > **文件位置**: `config/PermitAllUrlProperties.java` 这是一个巧妙的设计:实现 `InitializingBean` 接口,在 Spring 容器初始化完成后自动扫描所有 Controller 方法,找出标注了 `@Anonymous` 注解的 URL,将其路径中的 `{变量}` 替换为 `*` 通配符后汇总给 SecurityConfig 批量放行。 **好处**:新增加一个匿名接口时,只需要在方法上加 `@Anonymous` 注解,不需要手动修改 SecurityConfig。 ```java // 使用示例 @Anonymous @PostMapping("/login") public AjaxResult login(@RequestBody LoginBody loginBody) { ... } // 无需修改 SecurityConfig,自动生效! ``` ### 3.3 @Anonymous 注解 > **文件位置**: `annotation/Anonymous.java` ```java @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Anonymous { } ``` 可以标注在方法或整个 Controller 类上。标注在类上意味着该 Controller 的所有方法都匿名可访问。 --- ## 4. 认证流程 (Authentication) ### 4.1 完整登录流程 ``` 前端 login.html └─ POST /login {"username":"admin", "password":"admin123"} │ ▼ SysLoginController.login() │ ▼ SysLoginService.login(username, password, code, uuid) ├── loginPreCheck() — 参数校验(长度、非空) │ ├── authenticationManager.authenticate( │ new UsernamePasswordAuthenticationToken(username, password)) │ │ │ ▼ │ UserDetailsServiceImpl.loadUserByUsername(username) │ ├── userService.selectUserByUserName(username) // 从DB加载用户 │ ├── 检查:用户是否存在?是否被删除?是否被停用? │ ├── permissionService.getMenuPermission(user) // 计算权限 │ └── return new LoginUser(user, permissions) // 返回UserDetails │ │ │ ▼ │ DaoAuthenticationProvider │ ├── 比对密码(BCrypt.matches) │ └── 返回 Authentication 对象 │ ├── userService.updateLoginInfo() // 记录登录IP和时间 │ └── tokenService.createToken(loginUser) // 签发JWT │ ▼ 返回 {"code":200, "token":"eyJhbGciOi..."} 给前端 ``` ### 4.2 UserDetailsServiceImpl — 认证核心 > **文件位置**: `service/UserDetailsServiceImpl.java` ```java @Service public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) { // 1. 从数据库加载用户 SysUser user = userService.selectUserByUserName(username); // 2. 安全检查 if (user == null) → 抛出 "用户不存在" if (user is deleted) → 抛出 "用户已被删除" if (user is disabled) → 抛出 "用户已被停用" // 3. 创建 LoginUser(含权限) return new LoginUser(user, permissionService.getMenuPermission(user)); } } ``` **为什么不让 SysUser 直接实现 UserDetails?** 这是"适配器模式":SysUser 是纯数据实体,属于持久层;LoginUser 是安全层的适配对象。分层清晰,SysUser 不依赖 Spring Security 的接口。 ### 4.3 LoginUser — UserDetails 的实现 > **文件位置**: `domain/LoginUser.java` ```java public class LoginUser implements UserDetails { private SysUser user; // 原始用户实体 private Set permissions; // 权限标识集合 @Override public String getPassword() { return user.getPassword(); // 从SysUser获取密码 } @Override public String getUsername() { return user.getUserName(); // 从SysUser获取用户名 } @Override public Collection getAuthorities() { // 将权限标识字符串转为Spring Security的GrantedAuthority集合 return permissions.stream() .filter(Objects::nonNull) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); } // isAccountNonExpired / isAccountNonLocked 等全部返回 true // 本项目通过 del_flag 和 status 字段在 loadUserByUsername 阶段过滤 } ``` **权限标识** (如 `system:user:list`) 被转换为 `SimpleGrantedAuthority` 对象,Spring Security 在做 `hasAuthority()` / `hasRole()` 判断时会用到它们。 ### 4.4 密码加密 — BCrypt Spring Security 推荐使用 **BCryptPasswordEncoder**,主要特点: - **加盐自动化**:每次加密自动生成随机盐值,同一个密码两次加密产生不同的密文 - **抗彩虹表**:盐值让预计算的彩虹表攻击失效 - **可调节强度**:构造函数可传入 `strength` 参数(默认10),越大越安全但越慢 ```java // 加密 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String hash = encoder.encode("admin123"); // 结果示例: $2a$10$NkS2qJ3G7Z...(每次都不同) // 验证 boolean match = encoder.matches("admin123", hash); // true ``` --- ## 5. JWT Token 机制 ### 5.1 为什么用 JWT + 缓存混合模式 纯 JWT 的痛点: - **无法主动失效**:JWT 签发后,在过期前服务端无法让它失效(除非引入黑名单) - **体积膨胀**:如果把权限、角色都写入 JWT Payload,Token 会很大 本项目的混合方案: - **JWT Payload 只存 UUID**(轻量,约 50 字节) - **用户详情在服务端缓存**(TokenCacheService) - **踢人下线 = 删缓存**(立即生效,不需等 Token 过期) ### 5.2 Token 创建流程 ``` createToken(loginUser) ├── 生成 UUID(如 "550e8400-e29b-...") ├── setUserAgent(loginUser) → 记录IP、浏览器 ├── 将 LoginUser 存入 TokenCacheService └── 创建 JWT: Jwts.builder() .claims({"login_user_key": uuid, "username": "admin"}) .signWith(HMAC-SHA256密钥) .compact() ``` ### 5.3 Token 验证流程(每次请求) ``` JwtAuthenticationTokenFilter.doFilterInternal() ├── tokenService.getLoginUser(request) │ ├── 从 Header 取 "Authorization: Bearer xxx" │ ├── 去掉 "Bearer " 前缀 │ ├── 解析 JWT 获取 UUID │ └── 从缓存获取 LoginUser │ ├── 如果 LoginUser 不为 null 且当前未认证: │ ├── tokenService.verifyToken(loginUser) → 滑动续期 │ └── 创建 UsernamePasswordAuthenticationToken │ 存入 SecurityContextHolder │ └── chain.doFilter() → 继续过滤器链 ``` ### 5.4 滑动过期 ``` verifyToken(loginUser): 当前剩余时间 ≤ 20分钟 → refreshToken() 重新设置 loginTime = 当前时间 重新设置 expireTime = 当前时间 + 30分钟 更新缓存中的 LoginUser ``` **效果**:用户持续操作 → Token 不断续期;停止操作 30 分钟后 → 需要重新登录。 ### 5.5 TokenCacheService — 本地缓存 > **文件位置**: `service/TokenCacheService.java` 基于 `ConcurrentHashMap>` 实现的内存缓存: - 线程安全(ConcurrentHashMap) - 自动过期清理(ScheduledExecutorService 每 60 秒清理一次) - 支持 keys(pattern) 批量查找(如 admin 查看所有在线用户) > **生产建议**:替换为 Redis,支持分布式部署、持久化、集群共享。 以 Redis 替换 TokenCacheService 的示例: ```java // 替换前(本地缓存) @Autowired private TokenCacheService tokenCacheService; // 替换后(Redis) @Autowired private RedisTemplate redisTemplate; // setCacheObject → redisTemplate.opsForValue().set(key, value, timeout, unit) // getCacheObject → redisTemplate.opsForValue().get(key) // deleteObject → redisTemplate.delete(key) // keys → redisTemplate.keys(pattern) ``` --- ## 6. 授权与权限校验 (Authorization) ### 6.1 权限体系概览 ``` 数据库层 Spring Security 层 ───────── ───────────────── sys_user ──┐ LoginUser ├── sys_user_role ├── permissions: Set sys_role ──┘ │ │ 如 {"system:user:list", ├── sys_role_menu │ "system:user:query", ...} sys_menu ──┘ │ │ └── getAuthorities() └── perms 字段: → Set 如 "system:user:list" → Spring Security 判断依据 ``` ### 6.2 权限计算流程 > **核心类**: `SysPermissionService.getMenuPermission(user)` ``` getMenuPermission(SysUser user): ├── 如果是管理员(userId == 1) │ └── 直接返回 {"*:*:*"} — 超级权限通配符 │ └── 普通用户 ├── 获取用户的角色列表 (user.getRoles()) ├── 遍历每个角色: │ ├── 跳过已停用的角色 (status != "0") │ ├── 跳过 admin 角色(管理员已由上面处理) │ └── 查询角色关联的菜单权限 (sys_role_menu → sys_menu.perms) └── 返回所有权限标识的并集 ``` ### 6.3 PermissionService — @PreAuthorize 注解载体 > **文件位置**: `service/PermissionService.java` > **Bean 名称**: `"ss"`(取自 SpringSecurity 首字母) ```java @Service("ss") // Bean 名称为 "ss",在 @PreAuthorize 的 SpEL 中通过 @ss 引用 public class PermissionService { // 精确权限匹配 — 用户必须拥有指定权限 public boolean hasPermi(String permission) { LoginUser loginUser = SecurityUtils.getLoginUser(); return hasPermissions(loginUser.getPermissions(), permission); } // 任意权限匹配 — 用逗号分隔,满足其一即可 public boolean hasAnyPermi(String permissions) { for (String perm : permissions.split(",")) { if (hasPermi(perm)) return true; } return false; } // 角色匹配 public boolean hasRole(String role) { for (SysRole sysRole : loginUser.getUser().getRoles()) { if ("admin".equals(sysRole.getRoleKey()) || roleKey.equals(role)) { return true; } } return false; } // 任意角色匹配 public boolean hasAnyRoles(String roles) { ... } // 核心判断:检查权限集合是否包含目标权限或超级通配符 private boolean hasPermissions(Set permissions, String permission) { return permissions.contains("*:*:*") // 超级管理员 || permissions.contains(permission.trim()); // 精确匹配 } } ``` ### 6.4 @PreAuthorize 使用方式详解 #### 方式一:权限标识鉴权(推荐,最常用) ```java @PreAuthorize("@ss.hasPermi('system:user:list')") public AjaxResult userList() { ... } ``` - `@ss` → 引用 Bean 名称为 "ss" 的 PermissionService - `hasPermi('system:user:list')` → 调用 hasPermi 方法 - SpEL 表达式,Spring Security 的 MethodSecurityInterceptor 在方法执行前求值 - 返回 false → 抛出 `AccessDeniedException` → 返回 403 #### 方式二:角色鉴权 ```java @PreAuthorize("@ss.hasRole('admin')") public AjaxResult adminDashboard() { ... } ``` #### 方式三:任意权限匹配(OR 逻辑) ```java @PreAuthorize("@ss.hasAnyPermi('system:user:list,system:role:list')") public AjaxResult commonData() { ... } ``` 用户有 `system:user:list` **或** `system:role:list` 即可通过。 #### 方式四:编程式鉴权(代码中手动判断) ```java // 不使用注解,在代码中手动调用 PermissionService if (permissionService.hasPermi("system:user:delete")) { // 执行删除逻辑 } else { return AjaxResult.error("没有删除权限"); } ``` 比注解灵活,适合"条件组合判断"等复杂场景。 ### 6.5 权限命名规范 本项目的权限标识采用 **"模块:实体:操作"** 三段式命名: | 权限标识 | 含义 | |----------|------| | `system:user:list` | 系统模块 - 用户实体 - 列表查看 | | `system:user:query` | 系统模块 - 用户实体 - 查询 | | `system:user:add` | 系统模块 - 用户实体 - 新增 | | `system:user:edit` | 系统模块 - 用户实体 - 编辑 | | `system:user:remove` | 系统模块 - 用户实体 - 删除 | | `system:role:list` | 系统模块 - 角色实体 - 列表查看 | | `system:role:query` | 系统模块 - 角色实体 - 查询 | | `system:role:add` | 系统模块 - 角色实体 - 新增 | | `*:*:*` | 超级管理员通配符 — 拥有所有权限 | ### 6.6 JwtAuthenticationTokenFilter — 每次请求的身份还原 > **文件位置**: `security/filter/JwtAuthenticationTokenFilter.java` ```java @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { // 1. 从请求中还原 LoginUser LoginUser loginUser = tokenService.getLoginUser(request); // 2. 如果用户有效且当前未认证 if (loginUser != null && SecurityUtils.getAuthentication() == null) { // 3. 检查并刷新 Token(滑动过期) tokenService.verifyToken(loginUser); // 4. 创建认证令牌并存入 SecurityContext UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( loginUser, null, loginUser.getAuthorities()); authToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } // 5. 继续过滤链 chain.doFilter(request, response); } } ``` **为什么继承 `OncePerRequestFilter`?** 确保在一次请求中过滤器只执行一次——即使请求在内部被转发(forward)或包含(include)多次。 ### 6.7 完整请求鉴权链路 ``` HTTP 请求到达 │ ├── SecurityFilterChain(Spring Security 过滤器链) │ │ │ ├── JwtAuthenticationTokenFilter │ │ └── 提取 JWT → 还原用户 → 存入 SecurityContextHolder │ │ │ ├── AuthorizationFilter(URL 级别鉴权) │ │ └── 匹配 SecurityConfig 中的 authorizeHttpRequests 规则 │ │ │ └── Dispatch → Controller Method │ │ │ └── MethodSecurityInterceptor(方法级别鉴权) │ └── 执行 @PreAuthorize 中的 SpEL 表达式 │ └── 调用 PermissionService.hasPermi("xxx") │ ├── 从 SecurityContextHolder 获取 LoginUser │ ├── 获取 permissions 集合 │ └── 检查是否包含指定权限 │ ├── Yes → 执行 Controller 方法 │ └── No → AccessDeniedException → 403 ``` --- ## 7. RBAC 数据模型 ### 7.1 表结构关系 ``` sys_user (用户) sys_role (角色) sys_menu (菜单/权限) ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ user_id (PK) │ │ role_id (PK) │ │ menu_id (PK) │ │ user_name │ │ role_name │ │ menu_name │ │ password │ │ role_key │ │ perms │ ← 权限标识 │ nick_name │ │ role_sort │ │ menu_type │ M/C/F │ status │ │ status │ │ parent_id │ │ del_flag │ │ del_flag │ │ order_num │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ sys_user_role │ sys_role_menu │ │ ┌──────────────┐ │ ┌──────────────┐ │ ├───│ user_id (FK) │ ├────│ role_id (FK) │────┤ │ │ role_id (FK) │ │ │ menu_id (FK) │ │ │ └──────────────┘ │ └──────────────┘ │ │ 多对多关联表 │ 多对多关联表 │ ``` ### 7.2 菜单类型说明 | 类型 | 字母 | 说明 | 权限标识 | |------|------|------|----------| | 目录 | M | 分组容器,不渲染实际页面 | 无 | | 菜单 | C | 对应实际的页面路由 | 如 `system:user:list` | | 按钮 | F | 页面上的操作按钮 | 如 `system:user:add` | ### 7.3 已有测试数据的关系 ``` admin 用户 (user_id=1) └── admin 角色 (role_id=1) ├── 系统管理 (目录, menu_id=1) │ ├── 用户管理 (菜单, menu_id=100) │ │ ├── system:user:query (1000) │ │ ├── system:user:add (1001) │ │ ├── system:user:edit (1002) │ │ └── system:user:remove(1003) │ └── 角色管理 (菜单, menu_id=101) │ ├── system:role:query (1004) │ └── system:role:add (1005) user 用户 (user_id=2) └── common 角色 (role_id=2) ├── 系统管理 (目录, menu_id=1) │ ├── 用户管理 (菜单, menu_id=100) │ │ └── system:user:query (1000) ← 只能查询 │ └── 角色管理 (菜单, menu_id=101) │ └── system:role:query (1004) ← 只能查询 ``` --- ## 8. 前端权限集成 ### 8.1 登录流程 `login.html` → POST `/login` → 获取 Token → 存入 `localStorage` → 跳转 `index.html` ### 8.2 每次请求携带 Token ```javascript async function api(url) { const res = await fetch(url, { headers: { 'Authorization': 'Bearer ' + token } }); return res.json(); } ``` ### 8.3 获取用户权限 `/getInfo` 接口返回当前用户的: - `user`: 基本信息(用户名、昵称等) - `roles`: 角色集合(如 `["admin"]`) - `permissions`: 权限标识集合(如 `["system:user:list", "*:*:*"]`) 前端根据 `permissions` 集合判断哪些按钮/菜单显示: ```javascript const has = perms.includes('system:user:delete') || perms.includes('*:*:*'); if (has) { // 显示删除按钮 } ``` ### 8.4 关键安全原则 1. **前端权限仅是 UI 控制** — 真正的安全在后端 `@PreAuthorize` 2. **不要仅依赖前端隐藏按钮** — 攻击者可以直接调用 API 3. **前端和后端使用相同的权限标识** — 保证一致 --- ## 9. 快速启动指南 ### 9.1 环境要求 - JDK 17+ - MySQL 8.0+ - Maven 3.6+ ### 9.2 数据库准备 ```sql -- 创建数据库 CREATE DATABASE IF NOT EXISTS ry DEFAULT CHARACTER SET utf8mb4; -- 执行 RuoYi-Vue 的 SQL 脚本创建表结构(或使用项目中的 schema.sql) ``` ### 9.3 配置文件 编辑 `src/main/resources/application.yml`: ```yaml spring: datasource: url: jdbc:mysql://localhost:3306/ry?... # 你的数据库连接 username: root # 你的数据库用户名 password: root # 你的数据库密码 ``` ### 9.4 启动 ```bash cd 项目目录 mvn spring-boot:run ``` 首次启动会自动初始化测试数据(如果数据库中无数据)。 ### 9.5 访问 | 页面 | URL | 说明 | |------|-----|------| | 登录页 | `http://localhost:8080/login.html` | 可视化登录 | | 控制台 | `http://localhost:8080/index.html` | 权限测试面板 | | `/getInfo` | 需要 Token | 获取用户信息和权限 | | `/test/checkPerm?perm=xxx` | 需要 Token | 手动权限测试 | ### 9.6 测试账号 | 用户名 | 密码 | 角色 | 权限 | |--------|------|------|------| | admin | admin123 | 超级管理员 (admin) | `*:*:*` 所有权限 | | user | user123 | 普通角色 (common) | 仅查询权限 | --- ## 10. Spring Security 最佳实践与进阶 ### 10.1 几种常见鉴权模式对比 | 模式 | 实现方式 | 适用场景 | 本项目使用 | |------|----------|----------|-----------| | URL 级鉴权 | `authorizeHttpRequests` 配置 | 粗粒度,按路径拦截 | ✅ 登录/静态资源放行 | | 方法级鉴权 | `@PreAuthorize` | 中等粒度,按业务方法控制 | ✅ 主要使用方式 | | 注解式鉴权 | `@Secured`, `@RolesAllowed` | 简单角色判断 | ❌ 功能较弱 | | 编程式鉴权 | 代码中手动判断 | 复杂动态判断 | ✅ `/test/checkPerm` | | 数据级鉴权 | AOP + 数据过滤 | 不同角色看不同数据 | ❌ 本项目未涉及 | | 实例级鉴权 | ACL (Access Control List) | 对单个对象做权限 | ❌ 过于复杂 | ### 10.2 多角色判断 Spring Security 原生不支持 `@PreAuthorize("hasRole('A') and hasRole('B')")` 的 AND 逻辑(SpEL 中的 `hasRole` 只接受单个字符串)。通过在 PermissionService 中扩展自定义方法实现: ```java // 扩展:同时拥有多个角色 public boolean hasAllRoles(String roles) { for (String role : roles.split(",")) { if (!hasRole(role)) return false; } return true; } // 使用 @PreAuthorize("@ss.hasAllRoles('admin,editor')") ``` ### 10.3 数据权限(Data Scope) 本项目的角色表有 `data_scope` 字段但未实现。在 RuoYi 完整版中: - `data_scope=1`:全部数据权限 - `data_scope=2`:仅限本人数据 - `data_scope=3`:本部门数据 - `data_scope=4`:本部门及以下 可通过 MyBatis 拦截器或 AOP 在 SQL 查询时自动注入 `WHERE dept_id IN (...)` 条件来实现。 ### 10.4 生产环境注意事项 1. **替换 JWT 密钥**:配置文件中的 `token.secret` 默认值仅用于开发,生产环境需替换为 256-bit 随机密钥 2. **TokenCacheService → Redis**:本地缓存不支持分布式,多实例部署时需替换为 Redis 3. **HTTPS**:JWT 通过 HTTP Header 传输,在非 HTTPS 下会被中间人窃听 4. **Token 存储**:前端存储 Token 到 `localStorage` 有 XSS 风险,建议使用 `httpOnly` Cookie(需配合 CSRF 保护) 5. **异常信息**:生产环境不要向客户端返回详细的认证失败原因(如"用户不存在" vs "密码错误"),统一返回"用户名或密码错误"防止枚举攻击 6. **日志脱敏**:登录日志不要记录明文密码 ### 10.5 Spring Security 6.x vs 5.x 主要差异 | 功能 | Spring Security 5.x | Spring Security 6.x (本项目) | |------|---------------------|------------------------------| | 配置风格 | `http.antMatchers()` | `http.authorizeHttpRequests()` lambda | | 包名 | `javax.servlet` | `jakarta.servlet` | | `WebSecurityConfigurerAdapter` | 需继承 | 已移除,直接声明 Bean | | `@EnableGlobalMethodSecurity` | 使用此注解 | 改为 `@EnableMethodSecurity` | | 密码编码器 | 可选委托 | 建议显式声明 | ### 10.6 扩展建议 如果要在本项目基础上扩展,常见方向: 1. **添加验证码**:在 `/login` 之前增加 Captcha 校验 2. **添加记住我**:生成长期有效的 refresh token 3. **添加社交登录**:集成 Spring Security OAuth2 Client 4. **操作日志**:AOP 拦截所有 Controller 记录操作日志 5. **限流保护**:对 `/login` 接口添加登录频率限制(防爆破) 6. **多租户支持**:在权限标识前加租户前缀 --- ## License MIT