文章目录
-
- [Spring Boot 3.x + Spring Security 6.x 整合 JWT 实战:jjwt 与 java-jwt 两种工具库详解](#Spring Boot 3.x + Spring Security 6.x 整合 JWT 实战:jjwt 与 java-jwt 两种工具库详解)
-
- 一、前言
- [二、先了解 JWT](#二、先了解 JWT)
-
- [2.1 JWT 的认证流程](#2.1 JWT 的认证流程)
- [2.2 JWT 和 Session 有什么区别?](#2.2 JWT 和 Session 有什么区别?)
- [三、使用 jjwt 操作 JWT](#三、使用 jjwt 操作 JWT)
-
- [3.1 添加 Maven 依赖](#3.1 添加 Maven 依赖)
- [3.2 生成 JWT](#3.2 生成 JWT)
- [3.3 解析和验证 JWT](#3.3 解析和验证 JWT)
- [四、使用 java-jwt 操作 JWT](#四、使用 java-jwt 操作 JWT)
-
- [4.1 添加 Maven 依赖](#4.1 添加 Maven 依赖)
- [4.2 生成 JWT](#4.2 生成 JWT)
- [4.3 解析和验证 JWT](#4.3 解析和验证 JWT)
- [五、jjwt 和 java-jwt 有什么区别?](#五、jjwt 和 java-jwt 有什么区别?)
- [六、Spring Boot 3.x + Spring Security 6.x 集成 JWT](#六、Spring Boot 3.x + Spring Security 6.x 集成 JWT)
-
- [6.1 配置 SecurityFilterChain](#6.1 配置 SecurityFilterChain)
- [6.2 编写 JWT 认证过滤器](#6.2 编写 JWT 认证过滤器)
- [6.3 理解 SecurityContextHolder](#6.3 理解 SecurityContextHolder)
- [6.4 配置 AuthenticationManager](#6.4 配置 AuthenticationManager)
- [6.5 编写登录接口](#6.5 编写登录接口)
- 七、完整认证流程再串一下
- 八、总结
Spring Boot 3.x + Spring Security 6.x 整合 JWT 实战:jjwt 与 java-jwt 两种工具库详解
一、前言
之前做 Spring Boot 项目的时候,基本都会接触到用户登录和权限控制。比较常见的做法是使用 Spring Security,再配合 JWT 实现无状态认证。
刚开始接触 JWT 时,我其实也有一个疑问:既然都是生成和解析 Token,为什么还会有 jjwt 和 java-jwt 两个库?它们之间有什么区别?在 Spring Security 中又应该怎么接起来?
这篇文章就从实际使用的角度出发,分别介绍这两个工具库,再用 jjwt 配合 Spring Security 6 做一个简单的 JWT 认证流程。
本文使用的技术版本:
-
Spring Boot 3.x
-
Spring Security 6.x
-
jjwt 0.12.6
-
java-jwt 4.4.0
-
Java 17+
二、先了解 JWT
JWT,全称 JSON Web Token,是一种常见的身份认证方案。
它本身并不负责用户登录,也不负责权限管理。它主要解决的是:服务端生成一个 Token,客户端保存下来,之后请求接口时再把 Token 带上,服务端通过验证 Token 来确认请求者的身份。
一个 JWT 通常由三部分组成:
text
Header.Payload.Signature
分别是:
-
Header:存放 Token 类型和签名算法。
-
Payload:存放用户相关的数据,也就是声明(Claims)。
-
Signature:根据前两部分和密钥生成的签名,用来验证 Token 是否被篡改。
举个简单的例子,Payload 可能包含这样的信息:
json
{
"sub": "10001",
"username": "zhangsan",
"iat": 1730000000,
"exp": 1730003600
}
这里的 sub 一般用来表示用户主体,iat 是签发时间,exp 是过期时间。
有一点需要特别注意:JWT 默认只是编码,不是加密。
也就是说,别人拿到 Token 后,可以解码并读取 Payload 中的数据。因此,密码、身份证号等敏感信息不要直接放进 JWT。
2.1 JWT 的认证流程
整个过程并不复杂,可以拆成几个步骤:
-
用户输入账号和密码,向服务端发起登录请求。
-
服务端验证账号和密码。
-
验证通过后,生成 JWT 并返回给客户端。
-
客户端保存 Token。
-
后续请求在请求头中携带 Token。
-
服务端解析并验证 Token,确认身份后再处理请求。
请求头一般是这样的:
http
Authorization: Bearer <token>
服务端收到请求后,重点检查 Token 的签名和有效期。验证通过,才会把当前用户信息放进 Spring Security 的认证上下文中。
2.2 JWT 和 Session 有什么区别?
两者都能用于用户认证,只是实现方式不太一样。
| 对比项 | Session | JWT |
|---|---|---|
| 状态保存 | 服务端保存会话信息 | Token 中携带声明 |
| 客户端保存 | 通常保存 Session ID | 保存 JWT |
| 服务端存储 | 通常需要维护 Session | 可以不保存登录状态 |
| 扩展性 | 分布式环境需要处理 Session 共享 | 更适合无状态认证 |
| 注销处理 | 可以直接销毁 Session | 通常需要额外处理 |
| 适用场景 | 传统 Web 应用 | 前后端分离、移动端 API 等 |
JWT 并不是一定比 Session 好。比如需要立即注销 Token,或者需要实时修改用户权限时,就要额外考虑 Token 的失效机制。
如果只是普通的前后端分离项目,使用 JWT 是比较常见的选择。
三、使用 jjwt 操作 JWT
jjwt 是 Java 中比较常见的 JWT 工具库,主要用于生成、解析和验证 JWT。
项目地址:JJWT GitHub
3.1 添加 Maven 依赖
这里使用 jjwt 0.12.6。
需要注意,jjwt 从 0.12.x 开始,部分 API 和早期版本有所区别,下面的代码按照 0.12.x 的写法来写。
xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
这里拆成了三个依赖:
-
jjwt-api:提供 JWT 操作所需的 API。 -
jjwt-impl:具体的实现代码。 -
jjwt-jackson:负责 JSON 序列化和反序列化。
一般按照这个方式引入就可以了。
3.2 生成 JWT
先处理密钥。
这里使用 HMAC-SHA256 对 Token 进行签名。HS256 对密钥长度有要求,至少需要 256 bit,也就是 32 字节。
可以通过下面的代码生成一次密钥:
java
SecretKey key = Jwts.SIG.HS256.key().build();
String base64Key = Encoders.BASE64.encode(key.getEncoded());
System.out.println(base64Key);
生成后,把 Base64 字符串保存到配置文件或环境变量中。不要在每次应用启动时重新生成,否则之前签发的 Token 就无法通过新密钥验证。
例如:
yaml
jwt:
secret: ${JWT_SECRET}
接下来创建 JWT 工具类:
java
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
private SecretKey getKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 生成 JWT
*/
public String createToken(String userId) {
Date now = new Date();
Date expireTime = new Date(
now.getTime() + 1000 * 60 * 60
);
return Jwts.builder()
.subject(userId)
.issuedAt(now)
.expiration(expireTime)
.signWith(getKey(), Jwts.SIG.HS256)
.compact();
}
/**
* 解析并验证 JWT
*/
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(getKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
}
生成 Token 时,主要使用的是 Jwts.builder()。
几个比较常用的方法:
-
subject():设置 Token 的主体,一般放用户 ID。 -
issuedAt():设置签发时间。 -
expiration():设置过期时间。 -
signWith():指定密钥和签名算法。 -
compact():生成最终的 JWT 字符串。
这里设置的过期时间是一个小时。
3.3 解析和验证 JWT
解析 Token 使用的是:
java
Claims claims = Jwts.parser()
.verifyWith(getKey())
.build()
.parseSignedClaims(token)
.getPayload();
这里有几个地方值得注意。
verifyWith() 用来指定验证签名的密钥。
parseSignedClaims() 用于解析带签名的 Claims,同时会进行签名验证和相关声明校验。
如果 Token 的签名不正确,或者已经过期,解析时就会抛出异常。
获取用户 ID:
java
String userId = claims.getSubject();
获取过期时间:
java
Date expiration = claims.getExpiration();
实际项目中,解析 Token 时通常还需要处理异常。例如 Token 格式错误、签名不匹配、Token 过期等情况,都不能当作正常认证通过。
四、使用 java-jwt 操作 JWT
接下来看看另一个库:java-jwt。
它由 Auth0 提供,API 风格和 jjwt 有些区别,不过实现的功能基本相同。
项目地址:java-jwt GitHub
4.1 添加 Maven 依赖
这里使用 4.4.0 版本:
xml
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
相比 jjwt,java-jwt 的依赖配置简单一些,只需要引入一个依赖。
4.2 生成 JWT
先准备密钥。实际使用时,建议使用足够长度的随机密钥,不要直接把简单字符串当作生产环境密钥。
java
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
/**
* 生成 JWT
*/
public String createToken(String userId) {
Algorithm algorithm = Algorithm.HMAC256(secret);
return JWT.create()
.withSubject(userId)
.withIssuedAt(new Date())
.withExpiresAt(
new Date(System.currentTimeMillis()
+ 1000 * 60 * 60)
)
.sign(algorithm);
}
/**
* 验证并解析 JWT
*/
public DecodedJWT parseToken(String token) {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.build();
return verifier.verify(token);
}
}
生成 Token 的核心代码是:
java
JWT.create()
.withSubject(userId)
.withIssuedAt(new Date())
.withExpiresAt(expireTime)
.sign(algorithm);
这里的几个方法和 jjwt 比较类似:
-
withSubject():设置 Token 主体。 -
withIssuedAt():设置签发时间。 -
withExpiresAt():设置过期时间。 -
sign():使用指定算法进行签名。
最终返回的就是 JWT 字符串。
4.3 解析和验证 JWT
java-jwt 中,验证 Token 主要使用 JWTVerifier。
java
JWTVerifier verifier = JWT.require(algorithm)
.build();
DecodedJWT decodedJWT = verifier.verify(token);
验证通过后,就可以获取 Token 中的信息:
java
String userId = decodedJWT.getSubject();
Date expiresAt = decodedJWT.getExpiresAt();
Date issuedAt = decodedJWT.getIssuedAt();
这里有一个容易混淆的地方。
java-jwt 还提供了一个 JWT.decode() 方法:
java
DecodedJWT decodedJWT = JWT.decode(token);
这个方法只是解码 Token,并不会验证签名。
所以,不能拿 JWT.decode() 的结果直接作为用户身份认证的依据。真正用于认证时,应该调用 JWTVerifier.verify()。
五、jjwt 和 java-jwt 有什么区别?
前面分别写了两种工具库的基本用法。它们都能完成 JWT 的生成、解析和签名验证,实际项目中选择其中一个就够了。
简单对比一下:
| 对比项 | jjwt | java-jwt |
|---|---|---|
| 提供方 | JJWT 项目 | Auth0 |
| Maven 依赖 | 通常需要拆分多个模块 | 单个依赖 |
| 生成 Token | Jwts.builder() | JWT.create() |
| 解析 Token | Jwts.parser() | JWTVerifier.verify() |
| 获取 Claims | Claims | DecodedJWT |
| 解码但不验签 | 解析 API 有不同处理方式 | JWT.decode() |
| 使用特点 | API 比较完整,配置方式灵活 | API 简洁,使用直观 |
个人感觉,两者在普通业务项目中的使用差别并不大。
如果项目已经使用 jjwt,就没必要为了换库而专门迁移。新项目可以根据团队习惯选择。
还有一点:这两个库只负责 JWT 本身的操作。它们不会自动帮你完成 Spring Security 的用户认证和权限判断。
接下来把它们和 Spring Security 结合起来。
六、Spring Boot 3.x + Spring Security 6.x 集成 JWT
前面介绍的 jjwt 和 java-jwt,本质上都是 JWT 工具库。
而 Spring Security 负责的是另一部分工作:管理认证信息、处理认证流程、进行权限控制。
这两部分需要配合起来使用。
这里选择 jjwt 作为 Token 工具库,演示如何在 Spring Security 6 中接入 JWT。
6.1 配置 SecurityFilterChain
Spring Security 6 已经不再使用早期的 WebSecurityConfigurerAdapter。
现在主要通过 SecurityFilterChain 配置安全规则。
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
http
// 纯 Bearer Token API 场景下关闭 CSRF
.csrf(AbstractHttpConfigurer::disable)
// 不创建 Session
.sessionManagement(session -> session
.sessionCreationPolicy(
SessionCreationPolicy.STATELESS
)
)
// 配置接口访问权限
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/user/login",
"/user/register"
).permitAll()
.anyRequest().authenticated()
)
// 添加 JWT 认证过滤器
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
}
这里有几个配置需要理解一下。
- 关闭 CSRF
java
.csrf(AbstractHttpConfigurer::disable)
如果项目使用的是纯 Bearer Token 方式,也就是客户端通过请求头主动携带 JWT,通常可以关闭 CSRF 防护。
但如果把 JWT 放在 Cookie 中,就不能简单地认为关闭 CSRF 没有问题,还需要结合 Cookie 的发送方式进行判断。
- 设置无状态 Session
java
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
JWT 认证通常不依赖服务端 Session,所以这里设置为 STATELESS。
- 配置接口权限
java
.authorizeHttpRequests(auth -> auth
.requestMatchers("/user/login", "/user/register").permitAll()
.anyRequest().authenticated()
)
登录和注册接口不需要登录就能访问,其他接口必须通过认证。
- 添加 JWT 过滤器
java
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
)
这行代码很重要。
Spring Security 的认证流程是由过滤器链处理的。我们自定义 JWT 过滤器,并把它放到 UsernamePasswordAuthenticationFilter 之前。
这样,请求进入后就可以先检查请求头中的 JWT。
6.2 编写 JWT 认证过滤器
接下来是整个集成过程里比较关键的部分。
我们需要继承 OncePerRequestFilter,让 JWT 认证逻辑在请求过程中执行一次。
java
@Component
public class JwtAuthenticationFilter
extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String authorization =
request.getHeader("Authorization");
if (authorization == null
|| !authorization.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authorization.substring(7);
try {
// 解析 Token,验证签名和有效期
Claims claims = jwtUtil.parseToken(token);
// 获取用户 ID
String userId = claims.getSubject();
// 根据用户 ID 重新加载用户信息
UserDetails userDetails =
userDetailsService.loadUserByUsername(userId);
// 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
// 保存认证信息
SecurityContextHolder.getContext()
.setAuthentication(authentication);
} catch (JwtException | IllegalArgumentException e) {
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
}
这个过滤器主要做了几件事:
-
从请求头中获取 Authorization。
-
判断请求头是否以
Bearer开头。 -
截取真正的 JWT 字符串。
-
使用 jjwt 解析 Token,验证签名和有效期。
-
获取 Token 中的用户 ID。
-
根据用户 ID 查询用户信息和权限。
-
创建认证对象,放入
SecurityContextHolder。 -
继续执行后续过滤器。
这里重点说一下第六步。
为什么不直接从 Token 中获取用户信息,然后创建认证对象?
因为 Token 签发之后,用户的状态和权限可能发生变化。比如管理员修改了用户角色,或者用户账号被禁用。
如果每次请求都完全依赖 Token 中的旧权限,就可能出现权限没有及时更新的问题。
所以这里通过 UserDetailsService 重新加载用户信息和权限,再构建认证对象。
另外,示例中捕获无效 Token 异常后清空认证上下文,然后继续过滤器链。对于受保护接口,后续认证流程会将请求视为未认证请求,最终由 Spring Security 返回 401。
生产环境中可以进一步配置 AuthenticationEntryPoint,统一处理未认证请求的响应格式。
6.3 理解 SecurityContextHolder
过滤器中还有一行代码:
java
SecurityContextHolder.getContext()
.setAuthentication(authentication);
这行代码是 JWT 和 Spring Security 连接起来的关键。
JWT 工具库验证通过,只能说明 Token 本身有效。Spring Security 还需要知道当前请求对应的是哪个用户。
所以我们需要创建一个 Authentication 对象,再将它放进 SecurityContextHolder。
后续的权限判断就会使用这里保存的认证信息。
例如:
java
.requestMatchers("/admin/**").hasRole("ADMIN")
或者:
java
.requestMatchers("/user/**").hasAuthority("user:query")
这里的角色和权限,需要在 UserDetailsService 返回的 UserDetails 中正确配置。
如果构造认证对象时传入的是空权限集合,那么用户虽然可以通过 authenticated() 认证,但无法通过对应的角色或权限判断。
6.4 配置 AuthenticationManager
用户登录时,不能只接收账号和密码就直接生成 Token,还需要先验证密码是否正确。
Spring Security 提供了 AuthenticationManager,可以通过它完成认证。
在 Spring Security 6 中,可以通过 AuthenticationConfiguration 获取:
java
@Configuration
public class AuthenticationConfig {
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration configuration)
throws Exception {
return configuration.getAuthenticationManager();
}
}
这里还需要有对应的 UserDetailsService 和 PasswordEncoder 配置。
例如,密码使用 BCrypt 加密:
java
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
用户登录时,Spring Security 就可以根据配置的用户信息服务和密码编码器完成账号密码验证。
6.5 编写登录接口
有了 AuthenticationManager,就可以编写登录接口了。
先定义一个登录参数对象:
java
@Data
public class LoginRequest {
private String username;
private String password;
}
登录接口:
java
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public String login(@RequestBody LoginRequest request) {
// 验证账号和密码
Authentication authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
// 获取认证通过的用户信息
UserDetails userDetails =
(UserDetails) authentication.getPrincipal();
// 生成 JWT
String token = jwtUtil.createToken(
userDetails.getUsername()
);
return token;
}
}
登录过程可以理解为:
-
客户端提交用户名和密码。
-
AuthenticationManager接收认证请求。 -
Spring Security 调用用户信息服务查询用户。
-
使用
PasswordEncoder校验密码。 -
验证通过后返回认证结果。
-
根据用户信息生成 JWT。
-
将 Token 返回给客户端。
这里为了方便演示,直接返回 Token 字符串。实际项目中一般会封装成统一的响应对象,同时返回用户基本信息等内容。
七、完整认证流程再串一下
前面分别介绍了 JWT 工具类、Spring Security 配置、认证过滤器和登录接口。把它们放在一起,整个流程就是这样的:
#mermaid-svg-0pDPJA5KDmS6ZrOf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0pDPJA5KDmS6ZrOf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0pDPJA5KDmS6ZrOf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0pDPJA5KDmS6ZrOf .error-icon{fill:#552222;}#mermaid-svg-0pDPJA5KDmS6ZrOf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0pDPJA5KDmS6ZrOf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0pDPJA5KDmS6ZrOf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0pDPJA5KDmS6ZrOf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0pDPJA5KDmS6ZrOf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0pDPJA5KDmS6ZrOf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0pDPJA5KDmS6ZrOf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0pDPJA5KDmS6ZrOf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0pDPJA5KDmS6ZrOf .marker.cross{stroke:#333333;}#mermaid-svg-0pDPJA5KDmS6ZrOf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0pDPJA5KDmS6ZrOf p{margin:0;}#mermaid-svg-0pDPJA5KDmS6ZrOf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0pDPJA5KDmS6ZrOf .cluster-label text{fill:#333;}#mermaid-svg-0pDPJA5KDmS6ZrOf .cluster-label span{color:#333;}#mermaid-svg-0pDPJA5KDmS6ZrOf .cluster-label span p{background-color:transparent;}#mermaid-svg-0pDPJA5KDmS6ZrOf .label text,#mermaid-svg-0pDPJA5KDmS6ZrOf span{fill:#333;color:#333;}#mermaid-svg-0pDPJA5KDmS6ZrOf .node rect,#mermaid-svg-0pDPJA5KDmS6ZrOf .node circle,#mermaid-svg-0pDPJA5KDmS6ZrOf .node ellipse,#mermaid-svg-0pDPJA5KDmS6ZrOf .node polygon,#mermaid-svg-0pDPJA5KDmS6ZrOf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0pDPJA5KDmS6ZrOf .rough-node .label text,#mermaid-svg-0pDPJA5KDmS6ZrOf .node .label text,#mermaid-svg-0pDPJA5KDmS6ZrOf .image-shape .label,#mermaid-svg-0pDPJA5KDmS6ZrOf .icon-shape .label{text-anchor:middle;}#mermaid-svg-0pDPJA5KDmS6ZrOf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0pDPJA5KDmS6ZrOf .rough-node .label,#mermaid-svg-0pDPJA5KDmS6ZrOf .node .label,#mermaid-svg-0pDPJA5KDmS6ZrOf .image-shape .label,#mermaid-svg-0pDPJA5KDmS6ZrOf .icon-shape .label{text-align:center;}#mermaid-svg-0pDPJA5KDmS6ZrOf .node.clickable{cursor:pointer;}#mermaid-svg-0pDPJA5KDmS6ZrOf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0pDPJA5KDmS6ZrOf .arrowheadPath{fill:#333333;}#mermaid-svg-0pDPJA5KDmS6ZrOf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0pDPJA5KDmS6ZrOf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0pDPJA5KDmS6ZrOf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0pDPJA5KDmS6ZrOf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0pDPJA5KDmS6ZrOf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0pDPJA5KDmS6ZrOf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0pDPJA5KDmS6ZrOf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0pDPJA5KDmS6ZrOf .cluster text{fill:#333;}#mermaid-svg-0pDPJA5KDmS6ZrOf .cluster span{color:#333;}#mermaid-svg-0pDPJA5KDmS6ZrOf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0pDPJA5KDmS6ZrOf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0pDPJA5KDmS6ZrOf rect.text{fill:none;stroke-width:0;}#mermaid-svg-0pDPJA5KDmS6ZrOf .icon-shape,#mermaid-svg-0pDPJA5KDmS6ZrOf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0pDPJA5KDmS6ZrOf .icon-shape p,#mermaid-svg-0pDPJA5KDmS6ZrOf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0pDPJA5KDmS6ZrOf .icon-shape rect,#mermaid-svg-0pDPJA5KDmS6ZrOf .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0pDPJA5KDmS6ZrOf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0pDPJA5KDmS6ZrOf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0pDPJA5KDmS6ZrOf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
客户端提交账号和密码
登录接口
AuthenticationManager
验证用户信息和密码
认证是否通过
返回登录失败
jjwt 生成 JWT
返回 Token 给客户端
客户端携带 Bearer Token 请求接口
JwtAuthenticationFilter
解析并验证 JWT
Token 是否有效
认证失败
加载用户信息和权限
创建 Authentication
写入 SecurityContextHolder
Spring Security 进行权限判断
执行目标接口
这里最容易混淆的是 JWT 和 Spring Security 各自负责什么。
JWT 工具库负责:
-
生成 Token。
-
设置 Token 中的声明。
-
对 Token 签名。
-
解析 Token。
-
验证签名和有效期。
Spring Security 负责:
-
接收登录认证请求。
-
校验用户账号和密码。
-
管理认证对象。
-
保存当前请求的认证信息。
-
判断用户是否具有访问接口的权限。
两者各自负责一部分工作,配合起来才能完成完整的认证授权流程。
八、总结
这次主要介绍了两个 JWT 工具库:jjwt 和 java-jwt。
它们都可以完成 Token 的生成、解析和验证,使用方式有所不同,但核心思路差不多。
在 Spring Security 6 中,重点不是怎么生成一个 JWT,而是怎么把验证后的用户信息交给 Spring Security。
整个流程可以记成一句话:
登录时验证账号密码并生成 Token,请求时验证 Token,再把用户认证信息放进 SecurityContextHolder。
实际开发时,除了上面的基本流程,还需要考虑 Token 过期、刷新、主动注销、密钥管理以及用户权限变化等问题。
另外,本文代码主要用于说明集成思路。真正落地时,还需要结合项目的用户表、角色权限设计和异常处理机制进行调整。