ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Boot 3 + Spring Security 6 + JWT 打造 RBAC 权限系统

Spring Boot 3 + Spring Security 6 + JWT 打造 RBAC 权限系统 接手遗留系统权限模块时同事指着代码说“这里是地狱啊……”我一开始以为他在夸张直到开始梳理权限逻辑才明白这句话背后的含义。权限这块代码没有统一模型用户表、角色表、菜单表之间的关联散落在各种 SQL 里前端有的按钮能显示但接口不校验有的接口校验了却没有对应菜单。更麻烦的是新增一个角色要改十几处代码上线前权限变更往往需要逐个核对接口漏一个就出问题。这种状态非常典型很多团队都有类似的地狱级权限模块。本文想做的就是把这些混乱的权限需求收拢到一个标准模型里。我会围绕 Spring Boot 3 Spring Security 6 JWT MyBatis-Plus 这套技术栈从数据库表设计开始到认证过滤器、方法级权限注解、接口联调再到常见问题排查和工程建议完整搭建一套可运行、可维护的 RBAC 权限系统。如果你正准备做后台管理系统、毕业设计或者正在为老项目权限太乱而发愁这篇文章可以给你一套直接复用的方案。1. 先从概念说起权限系统为什么容易变成“地狱”1.1 权限系统真正的复杂度在哪里权限系统本身不是新东西难的是它和所有业务接口都有关系。你写的每一个接口理论上都要回答两个问题谁能访问这个接口以及他访问之后能操作哪些数据。前者叫功能权限后者叫数据权限。很多项目之所以乱就是把这两个问题搅在一起又没有一个统一模型来承载。功能权限相对好处理它描述的是“用户能不能执行某个操作”比如能不能查看用户列表、能不能创建订单、能不能导出报表。数据权限要难得多比如同样是查询订单销售只能看自己的订单销售经理能看整个团队的订单这已经超出了角色标签能解决的范畴。在中小型项目中大部分权限诉求集中在前者也就是功能权限。本文先围绕功能权限把模型和代码跑通数据权限放到最后的工程建议里专门讲。1.2 一个规范的权限模块应该长什么样一个规范的权限模块至少要具备这几个特征权限数据有清晰的表结构不会出现字段含义不明的情况权限判断集中在安全框架里而不是散落在每个接口的 if 判断中新增角色、调整权限不需要改代码只要改数据库关系即可接口的访问控制通过注解或配置声明式完成开发人员写接口时能一眼看出权限要求。要达到这些要求最常用的模型就是 RBAC即基于角色的访问控制。RBAC 的核心思想是不再直接把权限分配给用户而是先定义角色再把权限赋予角色最后把角色分配给用户。这样中间加了一层“角色”作为缓冲权限调整的灵活性会高很多。比如来了一个新员工只需要给他分配“运营”角色他自动就获得了该角色对应的所有菜单和按钮权限而不需要一条一条授权。1.3 本文的技术选型说明在技术选型上Spring Security 是 Spring 生态下最成熟的安全框架认证、授权、密码加密、会话管理都内置了虽然学习曲线陡一些但一旦跑通收益非常明显。JWT 负责在无状态接口场景下传递用户身份前后端分离项目非常适用。MyBatis-Plus 用来简化数据库操作国内使用率比较高示例代码也容易迁移到你自己的项目里。这套组合比较经典Spring Security 提供标准的认证授权链路JWT 解决登录状态的传递问题MyBatis-Plus 负责权限数据的查询。接下来我们先看开发环境然后从数据库表设计开始一步步把每个文件写出来。2. 环境准备与版本说明2.1 开发环境本文示例使用以下环境版本需要根据你的项目实际情况调整重点演示配置思路JDK17 或更高版本Maven3.8 或更高版本Spring Boot3.2.5Spring Security6.x由 Spring Boot 3.2.5 统一管理MyBatis-Plus3.5.7Spring Boot 3 版本MySQL8.xIDEIntelliJ IDEA如果你仍然使用 Spring Boot 2.x那么 MyBatis-Plus 的依赖需要换成mybatis-plus-boot-starterjavax.servlet包名也需要保持旧版风格。Spring Boot 3 已经把javax迁移到了jakarta这是升级时最常遇到的坑下面代码统一按 Spring Boot 3 编写。2.2 项目结构规划在动手写代码之前先明确项目结构。下面是比较清晰的分层方式src/main/java/com/example/demosecurity/ ├── config/SecurityConfig.java ├── controller/AuthController.java ├── controller/UserController.java ├── entity/SysUser.java ├── entity/SysMenu.java ├── mapper/SysUserMapper.java ├── service/UserDetailsServiceImpl.java ├── filter/JwtAuthenticationFilter.java ├── util/JwtUtil.java ├── dto/LoginRequest.java └── DemoApplication.java实体类只保留最核心的SysUser和SysMenu角色表、菜单角色关联表、用户角色关联表在 SQL 脚本中创建在查询时直接用 SQL 关联不需要全部映射成实体。这样代码量最小也足够把权限逻辑讲清楚。3. RBAC 权限模型设计3.1 为什么选择 RBAC 模型RBAC 模型在权限系统中的地位等同于 MVC 在架构设计中的地位属于“先定标准再实现”的经典方案。它把权限管理的核心对象拆成用户、角色、权限三部分用户和权限之间通过角色建立联系。这样做的好处非常直接权限调整从面向个人变成了面向角色人员的入职、转岗、离职都只需要调整用户与角色的绑定关系权限分配的工作量大幅下降。以用户列表接口为例传统做法是在接口里写if (user.isAdmin())或者在每个请求入口判断用户名是否在管理员列表中。这种写法在只有两三个接口时没什么问题但当接口数量增长到几十上百个权限判断逻辑就会分散到各个业务代码中缺乏统一管理。RBAC 的思路是把“谁能访问这个接口”的答案收敛到权限标识中接口只需要声明自己需要的权限标识安全框架统一校验。3.2 数据库表结构设计根据 RBAC 模型我们需要五张核心表用户表、角色表、菜单权限表、用户角色关联表、角色菜单关联表。菜单权限表在实际项目中通常还包含菜单名称、路由路径、组件路径、排序等字段本文只保留权限标识相关的核心字段避免示例过于臃肿。先创建数据库和用户表、角色表CREATE DATABASE demo_security DEFAULT CHARACTER SET utf8mb4; USE demo_security; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, nickname VARCHAR(50) COMMENT 昵称, status TINYINT DEFAULT 1 COMMENT 状态1启用0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) COMMENT 用户表; CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 角色ID, role_name VARCHAR(50) NOT NULL COMMENT 角色名称, role_key VARCHAR(50) NOT NULL COMMENT 角色标识如 admin ) COMMENT 角色表; CREATE TABLE sys_menu ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 菜单/权限ID, parent_id BIGINT DEFAULT 0 COMMENT 父级ID, name VARCHAR(50) NOT NULL COMMENT 菜单名称, perms VARCHAR(100) COMMENT 权限标识如 system:user:list, path VARCHAR(200) COMMENT 前端路由路径 ) COMMENT 菜单权限表; CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL COMMENT 用户ID, role_id BIGINT NOT NULL COMMENT 角色ID, PRIMARY KEY (user_id, role_id) ) COMMENT 用户角色关联表; CREATE TABLE sys_role_menu ( role_id BIGINT NOT NULL COMMENT 角色ID, menu_id BIGINT NOT NULL COMMENT 菜单ID, PRIMARY KEY (role_id, menu_id) ) COMMENT 角色菜单关联表;这里有几个设计细节值得注意。第一sys_menu里的perms字段是权限判断的依据它不是菜单名称而是形如system:user:list的权限标识。第二用户和角色是多对多关系一个用户可以拥有多个角色一个角色可以分配给多个用户所以需要中间表sys_user_role。第三角色和菜单权限也是多对多所以需要sys_role_menu。通过这三张关联表就能把用户最终拥有的权限串联起来。3.3 权限标识的命名规范perms字段的命名看起来像字符串但它实际上代表了业务含义。建议统一采用模块:操作:目标的格式例如system:user:list查看用户列表system:user:create新增用户system:user:update修改用户system:user:delete删除用户order:query:list查看订单列表这种命名规范在实战中非常重要。因为权限标识会出现在前端按钮的v-permission指令中也会出现在后端接口的PreAuthorize注解中还要写入数据库的sys_menu.perms字段。如果命名随意比如有的地方叫user:create有的地方叫addUser权限维护起来就是一场灾难。统一命名后开发和运维在排查权限问题时看到一个权限标识就能快速定位它属于哪个模块控制的是哪个操作。3.4 菜单权限与数据权限的边界很多初学者容易把菜单权限和数据权限混在一起导致表结构越设计越复杂。这里需要明确一点RBAC 模型解决的是“用户能不能看到某个菜单、能不能调用某个接口”的问题也就是功能权限。至于用户登录后只能看到自己负责的数据这是数据权限的范畴通常需要额外设计数据范围字段比如dept_id、owner_id并在查询语句中拼接数据过滤条件。把这两个概念分开数据库表结构会清爽很多。菜单权限走 RBAC 关系模型数据权限走业务表字段加查询拦截。本文演示的是功能权限的完整实现数据权限会在最后一章给出实践建议作为后续扩展方向。4. Spring Security JWT 核心原理拆解4.1 Spring Security 的过滤器链Spring Security 之所以看起来难以理解是因为它不像普通框架那样用几句代码完成功能而是通过一条过滤器链来工作。客户端请求进入应用后会按顺序经过多个过滤器每个过滤器只负责一件事有的解析基础认证信息有的校验 CSRF Token有的把用户信息放入 SecurityContext。只有当请求通过过滤器链后才会进入我们的 Controller。在 Spring Security 6 中我们通常通过一个SecurityFilterChainBean 来配置这条链。你可以关闭 CSRF、设置会话策略为无状态、放行登录接口、对剩余接口统一要求认证。认证过滤器会被我们手动插入到UsernamePasswordAuthenticationFilter之前这样请求在进入业务接口之前就已经完成了 JWT 的解析和身份信息的装载。这里有一个关键概念SecurityContextHolder。它是一个持有当前线程安全上下文的对象认证成功后我们可以把 Authentication 对象放入 SecurityContextHolder后续代码通过SecurityContextHolder.getContext().getAuthentication()就能拿到当前登录用户信息。Spring Security 的方法级权限注解本质上也是从 SecurityContextHolder 中读取 Authentication 的权限集合然后判断是否满足条件。4.2 JWT 与 Session 的取舍传统单体应用通常使用 Session 保存登录状态服务端把用户信息放在内存中给客户端一个 JSESSIONID 的 Cookie。这种方式在单一服务下很好用但前后端分离、分布式部署时就会遇到问题Session 默认存储在单机内存中用户请求落到另一台机器时就找不到登录状态必须引入 Session 共享组件比如 Redis。JWT 的解决思路完全不同。服务端认证成功后生成一段包含用户标识和过期时间的 Token把它返回给客户端。客户端后续每次请求都在请求头中携带这个 Token服务端通过签名验证 Token 的真伪并从 Token 中解析用户信息。服务端不需要存储会话状态所以天然支持分布式部署。但 JWT 也有明显的缺点。Token 签发后在有效期内无法主动撤销除非你维护一个黑名单。用户被禁用时已经签发的 Token 在到期之前可能仍然有效。因此实际项目中如果对账号封禁有强实时性要求就需要引入 Token 黑名单机制或者在每次请求时额外校验用户状态。本文示例在 JWT 过滤器中会重新加载一次用户信息顺手解决用户状态变更的问题虽然多了点数据库查询但换来了安全性。4.3 认证与授权流程把 Spring Security 的过滤器链和 JWT 结合起来完整的流程可以分为登录阶段和请求阶段。登录阶段客户端提交用户名密码到登录接口接口调用AuthenticationManager.authenticate()完成认证。AuthenticationManager内部会使用DaoAuthenticationProvider它会调用UserDetailsService.loadUserByUsername()加载用户信息再用PasswordEncoder比对密码。密码匹配后认证成功我们通过 JWT 工具类生成 Token 返回给客户端。请求阶段客户端携带 Token 访问受保护接口JwtAuthenticationFilter拦截请求从请求头解析出 Token验证签名和有效期然后从 Token 中取出用户名调用UserDetailsService重新加载用户信息和权限列表构建 Authentication 对象并放入 SecurityContextHolder。接下来请求继续沿着过滤器链走到方法级权限判断Spring Security 检查当前用户权限是否满足PreAuthorize注解声明的权限标识满足则执行方法否则抛出 403。5. 完整实战从零实现一个可运行的权限系统5.1 创建项目并添加依赖使用 IDEA 创建 Spring Boot 工程或者直接使用 Spring Initializr 生成。项目名为demo-security包名为com.example.demosecurity。创建完成后在pom.xml中添加以下依赖?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent groupIdcom.example/groupId artifactIddemo-security/artifactId version1.0.0/version namedemo-security/name descriptionSpring Security JWT RBAC 权限管理系统/description properties java.version17/java.version mybatis-plus.version3.5.7/mybatis-plus.version jjwt.version0.11.5/jjwt.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version${mybatis-plus.version}/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version${jjwt.version}/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version${jjwt.version}/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version${jjwt.version}/version scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project这里说明一下依赖的作用。spring-boot-starter-web提供 Web 环境spring-boot-starter-security提供认证授权能力mybatis-plus-spring-boot3-starter是 MyBatis-Plus 针对 Spring Boot 3 的专用 startermysql-connector-j是 MySQL 驱动jjwt系列库用于生成和解析 JWTLombok 用于简化实体类代码。如果你的项目不是 Spring Boot 3需要把 MyBatis-Plus 依赖换成mybatis-plus-boot-starter其他依赖保持不变。5.2 编写 application.yml 配置在src/main/resources目录下创建application.ymlserver: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo_security?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto jwt: secret: your-secret-key-please-change-in-production-abcdefghijklmnopqrstuvwxyz expire-minutes: 120jwt.secret是签名密钥这里需要特别注意JWT 使用 HS256 算法密钥长度必须至少 256 位也就是 32 个字节。示例中的密钥长度足够但生产环境一定要换成自己的随机字符串并且不要把密钥提交到 Git 仓库。推荐通过环境变量注入比如jwt.secret: ${JWT_SECRET:default-secret}这样不同环境可以使用不同的密钥。mybatis-plus.map-underscore-to-camel-case用来把数据库的下划线字段自动映射为驼峰属性例如create_time映射为createTime。id-type: auto表示主键使用数据库自增策略。5.3 编写实体类和 Mapper创建entity/SysUser.javapackage com.example.demosecurity.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; Data TableName(sys_user) public class SysUser { TableId(type IdType.AUTO) private Long id; private String username; private String password; private String nickname; private Integer status; }创建entity/SysMenu.javapackage com.example.demosecurity.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; Data TableName(sys_menu) public class SysMenu { TableId(type IdType.AUTO) private Long id; private Long parentId; private String name; private String perms; private String path; }然后创建mapper/SysUserMapper.java。这里除了继承BaseMapper获得单表 CRUD 能力还定义了一个查询权限标识的方法。权限查询需要关联三张表用户角色关联表、角色菜单关联表、菜单权限表最终得到该用户拥有的所有权限标识package com.example.demosecurity.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demosecurity.entity.SysUser; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; Mapper public interface SysUserMapper extends BaseMapperSysUser { Select( SELECT DISTINCT m.perms FROM sys_menu m INNER JOIN sys_role_menu rm ON m.id rm.menu_id INNER JOIN sys_user_role ur ON rm.role_id ur.role_id WHERE ur.user_id #{userId} AND m.perms IS NOT NULL AND m.perms ) ListString selectPermsByUserId(Param(userId) Long userId); }这个 SQL 使用查询权限也直接写成注解形式省去 XML 配置。SELECT DISTINCT是为了防止一个用户拥有多个角色而多个角色都包含同一个权限时出现重复数据。Spring Security 在构建权限集合时重复权限不会导致问题但保持数据干净总是好的。5.4 实现 JWT 工具类创建util/JwtUtil.java负责生成 Token、解析 Token、验证 Tokenpackage com.example.demosecurity.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire-minutes}) private long expireMinutes; private SecretKey getSigningKey() { return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } public String generateToken(String username) { Date now new Date(); Date expiryDate new Date(now.getTime() expireMinutes * 60 * 1000); return Jwts.builder() .setSubject(username) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } public String getUsernameFromToken(String token) { Claims claims Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); return claims.getSubject(); } public boolean validateToken(String token) { try { Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } }JWT 的subject字段用来存放用户名过期时间通过expire-minutes配置控制。validateToken方法把解析过程中可能出现的异常统一捕获包括签名错误、Token 过期、Token 格式非法等任何异常都视为无效 Token返回 false。注意一旦signWith的密钥长度不够JJWT 会抛出WeakKeyException所以配置密钥时一定要给足长度。5.5 实现 UserDetailsServiceSpring Security 的DaoAuthenticationProvider在登录认证时会调用UserDetailsService.loadUserByUsername()。我们创建一个service/UserDetailsServiceImpl.java从数据库加载用户信息并把权限标识转换成 Spring Security 的GrantedAuthority集合package com.example.demosecurity.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.demosecurity.entity.SysUser; import com.example.demosecurity.mapper.SysUserMapper; import org.springframework.security.authentication.DisabledException; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.List; Service public class UserDetailsServiceImpl implements UserDetailsService { private final SysUserMapper userMapper; public UserDetailsServiceImpl(SysUserMapper userMapper) { this.userMapper userMapper; } Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser userMapper.selectOne( new LambdaQueryWrapperSysUser() .eq(SysUser::getUsername, username) ); if (sysUser null) { throw new UsernameNotFoundException(用户不存在); } if (sysUser.getStatus() ! null sysUser.getStatus() 0) { throw new DisabledException(账号已被禁用); } ListString perms userMapper.selectPermsByUserId(sysUser.getId()); ListSimpleGrantedAuthority authorities perms.stream() .map(SimpleGrantedAuthority::new) .toList(); return new User(sysUser.getUsername(), sysUser.getPassword(), authorities); } }注意这里返回的User是 Spring Security 内置的org.springframework.security.core.userdetails.User它实现了UserDetails接口。构造参数分别是用户名、密码、权限集合。在登录认证阶段DaoAuthenticationProvider会取出这里返回的密码和用户输入的密码进行 BCrypt 比对。比对通过后这个 UserDetails 对象会成为 Authentication 的 principal后续 JWT 过滤器也会用到它。这里有一个安全细节loadUserByUsername()在 JWT 请求阶段也会被调用。也就是说即使某个用户拥有一个未过期的 Token但如果账号被管理员禁用重新加载用户时会抛出DisabledException这个请求就会认证失败。这样可以有效弥补 JWT 无法主动撤销的缺点代价是每次请求都多一次数据库查询性能要求高的场景可以引入缓存。5.6 实现 JWT 认证过滤器创建filter/JwtAuthenticationFilter.java。这个过滤器在每次请求时检查 Authorization 请求头如果存在合法 Token就构建 Authentication 对象放入 SecurityContextHolderpackage com.example.demosecurity.filter; import com.example.demosecurity.util.JwtUtil; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsService userDetailsService; public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) { this.jwtUtil jwtUtil; this.userDetailsService userDetailsService; } Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header request.getHeader(Authorization); if (header ! null header.startsWith(Bearer )) { String token header.substring(7); if (jwtUtil.validateToken(token)) { String username jwtUtil.getUsernameFromToken(token); UserDetails userDetails userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authentication); } else { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\token无效或已过期\}); return; } } filterChain.doFilter(request, response); } }如果请求头中没有 Authorization说明客户端没有携带 Token过滤器直接放行后续请求到达 Spring Security 的授权判断环节时因为没有 Authentication 对象会返回 401。如果 Token 无效直接在这里返回 401避免进入业务处理逻辑。5.7 配置 SecurityConfig创建config/SecurityConfig.java这是整个权限系统的核心配置类package com.example.demosecurity.config; import com.example.demosecurity.filter.JwtAuthenticationFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; Configuration EnableWebSecurity EnableMethodSecurity public class SecurityConfig { private final UserDetailsService userDetailsService; private final JwtAuthenticationFilter jwtAuthenticationFilter; public SecurityConfig(UserDetailsService userDetailsService, JwtAuthenticationFilter jwtAuthenticationFilter) { this.userDetailsService userDetailsService; this.jwtAuthenticationFilter jwtAuthenticationFilter; } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); provider.setPasswordEncoder(passwordEncoder()); return provider; } Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login).permitAll() .anyRequest().authenticated() ) .authenticationProvider(authenticationProvider()) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }逐个解释这些配置的含义。EnableMethodSecurity是 Spring Security 6 中开启方法级安全注解的入口对应旧版本的EnableGlobalMethodSecurity(prePostEnabled true)。没有这个注解PreAuthorize是不会生效的。csrf.disable()表示关闭 CSRF 防护。对于前后端分离且使用 JWT 的接口服务来说CSRF 防护的意义不大因为客户端不是通过 Cookie 自动携带认证信息而是手动在请求
返回列表