Spring Boot 整合 JWT:从入门到源码原理深度解析
-
- 前言
- [一、JWT 核心认知](#一、JWT 核心认知)
-
- [1.1 什么是 JWT](#1.1 什么是 JWT)
- [1.2 JWT 的结构](#1.2 JWT 的结构)
- [1.3 JWT 认证流程](#1.3 JWT 认证流程)
- [二、实战:Spring Boot 整合 JWT](#二、实战:Spring Boot 整合 JWT)
-
- [2.1 引入依赖](#2.1 引入依赖)
- [2.2 配置文件](#2.2 配置文件)
- [2.3 JWT 工具类封装](#2.3 JWT 工具类封装)
- [2.4 统一响应结果封装](#2.4 统一响应结果封装)
- [2.5 登录接口------生成 Token](#2.5 登录接口——生成 Token)
- [2.6 拦截器------统一 Token 校验](#2.6 拦截器——统一 Token 校验)
-
- [2.6.1 创建 JWT 拦截器](#2.6.1 创建 JWT 拦截器)
- [2.6.2 注册拦截器](#2.6.2 注册拦截器)
- [三、进阶:Spring Security + JWT](#三、进阶:Spring Security + JWT)
-
- [3.1 JWT 认证过滤器](#3.1 JWT 认证过滤器)
- [3.2 安全配置](#3.2 安全配置)
- 四、源码原理深度解析
-
- [4.1 JJWT 生成 Token 的底层流程](#4.1 JJWT 生成 Token 的底层流程)
- [4.2 拦截器的执行原理](#4.2 拦截器的执行原理)
- [4.3 关于 Token 存储的思考](#4.3 关于 Token 存储的思考)
- 五、最佳实践总结
-
- [5.1 核心要点回顾](#5.1 核心要点回顾)
- [5.2 安全注意事项](#5.2 安全注意事项)
- [5.3 原理一句话总结](#5.3 原理一句话总结)
前言
在前后端分离和微服务架构盛行的今天,传统的 Session 认证方式正面临着越来越多的挑战------Session 依赖服务器内存存储,分布式部署时存在会话共享问题,且跨域场景下 Cookie 传递受限。
JWT(JSON Web Token)作为一种无状态、自包含的身份认证方案,正逐渐成为业界主流。它让服务端无需存储会话信息,每次请求只需验证令牌本身即可完成身份认证,天然适配分布式和前后端分离架构。
本文将从代码实践到源码原理,全面讲解 Spring Boot 整合 JWT 的实现方式。
一、JWT 核心认知
1.1 什么是 JWT
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间以 JSON 对象的形式安全地传输信息。它通常用于用户身份认证、信息交换、单点登录(SSO)和 API 接口鉴权。
核心优势:
| 优势 | 说明 |
|---|---|
| 无状态(Stateless) | 服务端无需存储会话信息,减轻服务器压力 |
| 自包含(Self-contained) | 所有用户信息都包含在令牌中 |
| 跨域支持 | 天然支持前后端分离、微服务架构 |
| 可扩展性好 | 支持自定义声明(Claims) |
1.2 JWT 的结构
一个 JWT 令牌由三部分组成,用 . 分隔:xxxxx.yyyyy.zzzzz。
(1)Header(头部)
包含令牌类型和签名算法:
json
{
"alg": "HS256",
"typ": "JWT"
}
alg:签名算法,如 HS256(HMAC SHA-256)、RS256(RSA)typ:令牌类型,固定为 JWT
(2)Payload(负载)
包含"声明(Claims)"------关于用户和附加信息的 JSON 数据。分为三类:
| 类型 | 说明 | 示例 |
|---|---|---|
| Registered Claims | 预定义标准字段(非强制) | exp(过期时间)、iss(发行人)、sub(主题) |
| Public Claims | 公共字段,建议使用命名空间 | https://example.com/role |
| Private Claims | 自定义私有字段 | userId、username、role |
⚠️ 重要提醒 :Header 和 Payload 仅经过 Base64 编码,并未加密 ,任何人都可以解码查看内容。千万不要在 Payload 中存放密码等敏感信息。
(3)Signature(签名)
用于验证令牌完整性和防止篡改。生成方式:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secretKey
)
1.3 JWT 认证流程
整个认证流程分为四步:
- 登录认证:客户端提交用户名/密码,服务器验证通过后生成 JWT Token 返回
- 存储令牌:客户端接收 Token 后存储在本地(localStorage、Cookie 等)
- 携带令牌 :后续所有请求都在请求头(
Authorization: Bearer <token>)中携带 Token - 验证令牌:服务器拦截请求,解析并验证 Token 的有效性(签名、过期时间等)
二、实战:Spring Boot 整合 JWT
2.1 引入依赖
使用 JJWT(Java JWT)库来简化 JWT 的生成、解析和验证操作。推荐使用 0.11.5 版本,兼容 Spring Boot 2.x/3.x:
xml
<!-- JJWT 核心 API -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- JJWT 运行时实现 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- JJWT Jackson 序列化支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
2.2 配置文件
在 application.yml 中配置 JWT 相关参数:
yaml
jwt:
secret: your-secret-key-please-change-in-production
expire: 7200000 # 2小时,单位毫秒
2.3 JWT 工具类封装
工具类封装了生成 Token、解析 Token、验证 Token 三大核心方法:
java
package com.example.demo.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
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;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expire}")
private Long expire;
/**
* 获取签名密钥
*/
private SecretKey getSecretKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
/**
* 生成 JWT Token
*/
public String generateToken(Long userId, String username, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
claims.put("role", role);
return Jwts.builder()
.setClaims(claims) // 载荷信息
.setIssuedAt(new Date()) // 签发时间
.setExpiration(new Date(System.currentTimeMillis() + expire)) // 过期时间
.signWith(getSecretKey(), SignatureAlgorithm.HS256) // 签名算法
.compact(); // 拼接生成最终令牌
}
/**
* 解析 JWT Token,获取 Claims
*/
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token)
.getBody(); // 获取 Payload 部分
}
/**
* 验证 Token 是否有效
*/
public boolean validateToken(String token) {
try {
Claims claims = parseToken(token);
// 检查是否过期
Date expiration = claims.getExpiration();
if (expiration != null && expiration.before(new Date())) {
log.warn("Token 已过期");
return false;
}
return true;
} catch (Exception e) {
log.error("Token 验证失败:{}", e.getMessage());
return false;
}
}
/**
* 从 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);
}
}
代码要点说明:
Keys.hmacShaKeyFor():将字符串密钥转换为SecretKey对象,这是 JJWT 0.11.x 版本的标准做法setClaims():设置自定义载荷内容setExpiration():设置过期时间,单位毫秒signWith():指定签名算法和密钥parseClaimsJws():解析签名后的 JWT(带签名的完整令牌)compact():将各部分拼接成最终的令牌字符串
2.4 统一响应结果封装
java
package com.example.demo.common;
import lombok.Data;
@Data
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(int code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}
2.5 登录接口------生成 Token
java
package com.example.demo.controller;
import com.example.demo.common.Result;
import com.example.demo.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
@Autowired
private JwtUtils jwtUtils;
@PostMapping("/login")
public Result<String> login(@RequestParam String username,
@RequestParam String password) {
// 实际项目中这里需要查询数据库验证用户名和密码
// 此处仅做演示,假设用户名为 admin,密码为 123456
if ("admin".equals(username) && "123456".equals(password)) {
// 验证通过,生成 Token
// 实际项目中 userId 和 role 应从数据库查询
String token = jwtUtils.generateToken(1L, username, "ADMIN");
return Result.success(token);
}
return Result.error(401, "用户名或密码错误");
}
}
2.6 拦截器------统一 Token 校验
拦截器的位置:位于过滤器和控制器之间,是 Spring MVC 框架的一部分。
用一句话比喻:
- 过滤器(Filter) :像大楼的保安,检查每个进入大楼的人
- 拦截器(Interceptor) :像公司前台,专门处理公司内部事务
- 控制器(Controller) :像各个部门,处理具体的业务请求
2.6.1 创建 JWT 拦截器
java
package com.example.demo.interceptor;
import com.example.demo.utils.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Slf4j
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtils jwtUtils;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 步骤1:获取请求 URL
String requestURL = request.getRequestURL().toString();
log.info("拦截到请求: {}", requestURL);
// 步骤2:判断是否为登录请求,登录请求放行
if (requestURL.contains("/login")) {
log.info("登录请求,直接放行");
return true;
}
// 步骤3:从请求头中获取 Token
String token = request.getHeader("Authorization");
log.info("获取到的 Token: {}", token);
// 步骤4:检查 Token 是否存在
if (token == null || token.isEmpty()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未提供 Token\"}");
return false;
}
// 步骤5:验证 Token
// 如果 Token 以 "Bearer " 开头,需要去掉前缀
if (token.startsWith("Bearer ")) {
token = token.substring(7);
}
if (!jwtUtils.validateToken(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"Token 无效或已过期\"}");
return false;
}
// Token 验证通过,可以将用户信息存入 ThreadLocal 供后续使用
// 例如:UserContext.setCurrentUser(jwtUtils.getUserIdFromToken(token));
log.info("Token 验证通过,放行请求");
return true;
}
}
拦截器执行流程:
客户端请求 → 过滤器(Filter) → 拦截器(Interceptor) → 控制器(Controller)
↑
preHandle() 执行 JWT 校验
2.6.2 注册拦截器
注意 :必须通过配置类注册拦截器,不能直接 new 对象,否则 Spring 无法管理依赖注入。
java
package com.example.demo.config;
import com.example.demo.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/login"); // 排除登录接口
}
}
三、进阶:Spring Security + JWT
如果项目中使用了 Spring Security,可以在其过滤器链中集成 JWT。
3.1 JWT 认证过滤器
java
package com.example.demo.filter;
import com.example.demo.utils.JwtUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
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 {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 获取 Token
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
}
// 验证 Token
if (token != null && jwtUtils.validateToken(token)) {
String username = jwtUtils.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 将用户信息存入 SecurityContext
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
3.2 安全配置
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
四、源码原理深度解析
4.1 JJWT 生成 Token 的底层流程
当调用 Jwts.builder() 生成 Token 时,底层经历了以下步骤:
Jwts.builder()
.setClaims(claims) → 将 Map 转换为 JSON,放入 Payload
.setExpiration(date) → 将过期时间加入 Payload(标准字段 exp)
.signWith(key, HS256) → 使用 HMAC-SHA256 对 Header+Payload 签名
.compact() → Base64Url(Header) + "." + Base64Url(Payload) + "." + Signature
签名原理:
Signature = HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secretKey
)
服务器收到 Token 后,用同样的密钥重新计算签名,与 Token 中的签名比对------如果一致,说明 Token 未被篡改。
4.2 拦截器的执行原理
Spring MVC 中,拦截器的执行依赖于 DispatcherServlet 的 doDispatch() 方法:
doDispatch() {
// 1. 获取 HandlerExecutionChain(包含拦截器链)
HandlerExecutionChain mappedHandler = getHandler(request);
// 2. 执行所有拦截器的 preHandle()
if (!mappedHandler.applyPreHandle(request, response)) {
return; // 任一拦截器返回 false,请求被拦截
}
// 3. 执行 Controller 方法
mv = ha.handle(request, response, mappedHandler.getHandler());
// 4. 执行所有拦截器的 postHandle()
mappedHandler.applyPostHandle(request, response, mv);
// 5. 渲染视图后执行 afterCompletion()
}
JWT 拦截器正是在 applyPreHandle() 阶段完成 Token 校验 ,返回 false 则请求被拦截,不会到达 Controller。
4.3 关于 Token 存储的思考
JWT 本身是无状态的,但为了增强安全性,业界有一些实践:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 纯 JWT(无存储) | 完全无状态,扩展性好 | Token 无法主动失效 |
| JWT + Redis 黑名单 | 可主动失效 Token | 引入额外存储 |
| JWT + Refresh Token | 用户体验好,可无感刷新 | 实现复杂 |
五、最佳实践总结
5.1 核心要点回顾
| 组件 | 作用 |
|---|---|
JwtUtils |
封装 JWT 的生成、解析、验证三大核心方法 |
JwtInterceptor |
实现 HandlerInterceptor,在 preHandle() 中统一校验 Token |
WebConfig |
注册拦截器,配置拦截路径和排除路径 |
5.2 安全注意事项
- 密钥管理:确保密钥的安全保管,避免泄露。生产环境应放在配置中心或环境变量中
- 过期时间设置:合理设置 JWT 的有效期,既要保证安全性,又要避免频繁重新登录
- 敏感信息保护 :绝对不要在 Payload 中存放密码等敏感信息
- 异常处理:妥善处理验证过程中可能出现的各种异常
5.3 原理一句话总结
JWT 本质上是一种将用户身份信息编码为三段式字符串(Header + Payload + Signature)的令牌规范 :Header 声明签名算法,Payload 承载用户信息,Signature 通过密钥对前两部分进行签名以确保完整性。Spring Boot 整合 JWT 的核心是通过拦截器在请求到达 Controller 之前统一校验 Token,验证通过后放行,否则拦截返回错误信息。