news 2026/8/31 2:13:33

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 开发环境

本文示例使用以下环境,版本需要根据你的项目实际情况调整,重点演示配置思路:

  • JDK:17 或更高版本
  • Maven:3.8 或更高版本
  • Spring Boot:3.2.5
  • Spring Security:6.x(由 Spring Boot 3.2.5 统一管理)
  • MyBatis-Plus:3.5.7(Spring Boot 3 版本)
  • MySQL:8.x
  • IDE:IntelliJ 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

实体类只保留最核心的SysUserSysMenu,角色表、菜单角色关联表、用户角色关联表在 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_idowner_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 version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>demo-security</artifactId> <version>1.0.0</version> <name>demo-security</name> <description>Spring Security + JWT + RBAC 权限管理系统</description> <properties> <java.version>17</java.version> <mybatis-plus.version>3.5.7</mybatis-plus.version> <jjwt.version>0.11.5</jjwt.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>${jjwt.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>${jjwt.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>${jjwt.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-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 的专用 starter,mysql-connector-j是 MySQL 驱动,jjwt系列库用于生成和解析 JWT,Lombok 用于简化实体类代码。如果你的项目不是 Spring Boot 3,需要把 MyBatis-Plus 依赖换成mybatis-plus-boot-starter,其他依赖保持不变。

5.2 编写 application.yml 配置

src/main/resources目录下创建application.yml

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo_security?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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: 120

jwt.secret是签名密钥,这里需要特别注意:JWT 使用 HS256 算法,密钥长度必须至少 256 位,也就是 32 个字节。示例中的密钥长度足够,但生产环境一定要换成自己的随机字符串,并且不要把密钥提交到 Git 仓库。推荐通过环境变量注入,比如jwt.secret: ${JWT_SECRET:default-secret},这样不同环境可以使用不同的密钥。

mybatis-plus.map-underscore-to-camel-case用来把数据库的下划线字段自动映射为驼峰属性,例如create_time映射为createTimeid-type: auto表示主键使用数据库自增策略。

5.3 编写实体类和 Mapper

创建entity/SysUser.java

package 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.java

package 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 BaseMapper<SysUser> { @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 <> '' """) List<String> selectPermsByUserId(@Param("userId") Long userId); }

这个 SQL 使用查询权限也直接写成注解形式,省去 XML 配置。SELECT DISTINCT是为了防止一个用户拥有多个角色,而多个角色都包含同一个权限时出现重复数据。Spring Security 在构建权限集合时,重复权限不会导致问题,但保持数据干净总是好的。

5.4 实现 JWT 工具类

创建util/JwtUtil.java,负责生成 Token、解析 Token、验证 Token:

package 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 实现 UserDetailsService

Spring 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 LambdaQueryWrapper<SysUser>() .eq(SysUser::getUsername, username) ); if (sysUser == null) { throw new UsernameNotFoundException("用户不存在"); } if (sysUser.getStatus() != null && sysUser.getStatus() == 0) { throw new DisabledException("账号已被禁用"); } List<String> perms = userMapper.selectPermsByUserId(sysUser.getId()); List<SimpleGrantedAuthority> 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 对象放入 SecurityContextHolder:

package 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;charset=UTF-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 自动携带认证信息,而是手动在请求

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 2:09:34

软件供应链溯源实战:TeamPCP团伙头像指纹穿透与OPSEC漏洞复盘

本文为2026年最新高危供应链攻击完整复盘实战专栏&#xff0c;无AI模板化套话、无空洞理论堆砌。全文基于Flare、安天CERT、Phoenix Security公开溯源日志、澳洲警方庭审公示信息、GitHub恶意载荷源码拆解编写。完整还原TeamPCP团伙从技术攻坚到身份暴露的全链路&#xff0c;公…

作者头像 李华
网站建设 2026/8/31 2:07:21

C语言程序结构全解析:从源码到运行的编译链接与内存布局

很多人第一次接触 C 语言&#xff0c;都是从一个“Hello World”开始的。IDE 自动生成模板、点一下编译、运行&#xff0c;黑窗口弹出“Hello World”&#xff0c;然后教程告诉你&#xff0c;这就是 C 语言的程序结构。但等到你真正开始在洛谷或 PTA 上刷题&#xff0c;或者在课…

作者头像 李华
网站建设 2026/8/31 2:07:05

LPL辅助“超模”背后:从赛制到数据复盘的全解析

一场 2 比 1 的系列赛&#xff0c;如果只看最终比分&#xff0c;很难理解赛后讨论为什么会集中到一位辅助选手身上。这次 LPL 职业联赛第三赛段组内赛&#xff0c;WBG 对阵 NIP&#xff0c;最终比分 WBG 1 比 2 NIP。比分是公开事实&#xff0c;但真正让评论区热闹起来的&#…

作者头像 李华
网站建设 2026/8/31 2:05:35

基于Simulink的12bit 200MSPS流水线ADC行为级建模与FFT测试实践

简介&#xff1a;本资源是一套面向电子工程、信号处理及高速ADC设计方向的高校师生与硬件工程师的仿真验证工具包&#xff0c;聚焦12位200MHz流水线ADC的建模与频域性能评估。资源包含1个Simulink系统级模型文件&#xff08;.mdl&#xff09;和1个MATLAB测试脚本&#xff08;.m…

作者头像 李华
网站建设 2026/8/31 2:05:33

用Keras和深度学习解码宇宙信号:从频谱图到分类模型

用深度学习和 Keras 解码宇宙信号&#xff0c;听起来像是要处理什么外星文明电报&#xff0c;但落到工程上其实很朴素&#xff1a;把望远镜采集到的原始数据经过变换之后交给神经网络&#xff0c;让模型回答“这个信号是不是真实天体信号”“是脉冲星候选体还是地面干扰”“这一…

作者头像 李华
网站建设 2026/8/31 2:05:26

技术博客撰写前的信息准备指南

这个标题缺少撰写一篇技术博客所需的核心信息&#xff0c;无法直接生成合规、可复现的教程长文。主要原因是&#xff1a;项目正文、关键词、摘要描述均为空&#xff0c;全文没有任何技术主题、功能说明或代码背景。“Jason Liu”和“某工具”都无法核实&#xff0c;既不清楚指代…

作者头像 李华