JWT,全称 JSON Web Token,是目前最流行的跨域身份验证解决方案之一。
在深入探讨 JWT 之前,我们不妨先回顾一下传统的 session 和基于缓存(如 Redis)的 token 认证方案,以及它们在分布式架构下遇到的瓶颈。
在《Session、Cookie 与 Token 的工作原理及区别》一文中对 session、cookie 和 token 有更具体的介绍。
传统Session认证的局限性
传统的 session 交互流程如下图所示:

当浏览器向服务器发起登录请求并验证通过后,用户信息会被存储在服务器内存的 session 中。随后,服务器会生成一个 sessionId 并将其放入响应头的 Set-Cookie 字段,返回给浏览器。
此后,浏览器每次发送请求时,都会自动在请求头的 Cookie 中携带这个 sessionId。

服务器通过这个 sessionId 就能查询到对应的 session 信息,从而判断用户是否登录以及获取其身份。
在单服务器架构下,这种模式运作良好。然而,当业务量激增,需要部署多台服务器并通过负载均衡对外服务时,问题就出现了。不同服务器之间的 session 默认是隔离的,用户可能在服务器 A 登录成功,但下次请求被分发到服务器 B 时,却找不到对应的 session 信息,导致需要重新登录,严重影响用户体验。
基于Token(缓存)的方案
为了解决上述问题,引入了基于 token 的方案,其流程如下:

在这种方案中,服务器验证用户身份后,会生成一个唯一的 token 返回给客户端。同时,服务器会将这个 token 与用户信息的映射关系存储在如 Redis 这样的分布式缓存中。客户端后续请求时需携带此 token,服务器通过验证缓存中是否存在该 token 来判断其有效性。
这种方案保证了服务的无状态性,易于水平扩展。但无论是 session 还是这种 token 方案,在集群环境下都依赖于 redis 这类第三方缓存来实现数据共享。
那么,有没有一种方案,不需要依赖缓存数据库,就能实现用户信息在集群间的“一次登录,处处可见”呢?答案就是 JWT。
什么是 JWT?
JWT 的主要原理是:服务器在认证用户通过后,生成一个包含用户信息的 JSON 对象,并对其进行签名后发回给用户。一个简化的示例如下:
{
"sub": "login",
"userName": "Dylan Smith",
"userId": "1",
"userEmail": "junfeng0828@gmail.com",
"iat": 1730049826
}
之后,用户与服务器通信时,必须在请求(通常在 Authorization 头)中携带这个经过签名的字符串(即 JWT)。服务器通过验证签名来识别用户身份,并直接从 JWT 中读取用户信息,无需查询任何会话存储。这使得服务器完全无状态,易于扩展。
重要提醒:由于 JWT 的负载(Payload)可以被客户端解码,因此切勿在其中存储任何敏感信息(如密码)。
完整的 JWT 认证流程可概括为下图:

JWT 的组成格式
一个 JWT 由三部分组成,以点(.)分隔:Header(头部)、Payload(负载)和 Signature(签名)。形式如下:
Header.Payload.Signature

头部通常是一个 JSON 对象,描述 token 类型和签名算法,例如:
{
"alg": "HS256",
"typ": "JWT"
}
该 JSON 经过 Base64URL 编码后形成 JWT 的第一部分。
2. Payload
负载部分也是一个 JSON 对象,用于存放实际需要传递的数据。JWT 官方定义了 7 个标准字段(可选),如签发者 (iss)、过期时间 (exp)、主题 (sub) 等。你还可以在此定义自定义的私有字段。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
注意:JWT 默认是不加密的,任何人都可以解码看到这部分内容,所以不要存放机密信息。该 JSON 同样会经过 Base64URL 编码,形成 JWT 的第二部分。
3. Signature
签名部分是对前两部分的签名,用于防止数据被篡改。生成签名的算法在 Header 中指定(如 HS256),计算方式如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
其中 secret 是只有服务器知道的密钥。计算出的签名再经过 Base64URL 编码,形成 JWT 的第三部分。
最后,将编码后的三部分用点连接起来,就得到了完整的 JWT。
4. Base64URL 编码
Header 和 Payload 使用的 Base64URL 算法与标准 Base64 类似,但为了适应 URL 环境,它将字符 + 替换为 -,/ 替换为 _,并省略末尾的填充字符 =。
SpringBoot 整合 JWT 实践
理论讲完,接下来我们动手在 Spring Boot 项目中整合 JWT。
第一步:添加依赖
首先创建一个 Spring Boot 项目,并在 pom.xml 中添加 JWT 依赖(这里使用 java-jwt 库):
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
第二步:定义用户信息载体
创建一个 UserToken 类,用于封装将要存入 JWT 的用户信息:
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class UserToken implements Serializable {
private static final long serialVersionUID = 1L;
private String userId;
private String userEmail;
private String userName;
}
第三步:编写 JWT 工具类
创建 JwtTokenUtil 工具类,负责生成和验证 Token:
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import java.util.Date;
public class JwtTokenUtil {
// 定义 token 返回头部
public static final String AUTH_HEADER_KEY = "Jwt";
public static final String TOKEN_PREFIX = "Dylan ";
public static final String KEY = "test_key";
public static final Long EXPIRATION_TIME = 1000L * 60 * 60; // 1小时
/**
* 创建 TOKEN
* @param content
* @return
*/
public static String createToken(String content) {
return TOKEN_PREFIX + JWT.create()
.withSubject(content)
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.sign(Algorithm.HMAC512(KEY));
}
/**
* 验证 token
* @param token
*/
public static String verifyToken(String token) throws Exception {
try {
return JWT.require(Algorithm.HMAC512(KEY))
.build()
.verify(token.replace(TOKEN_PREFIX, ""))
.getSubject();
} catch (TokenExpiredException e) {
throw new Exception("token已经过期,请重新登录", e);
} catch (JWTVerificationException e) {
throw new Exception("token验证失败!", e);
}
}
}
第四步:配置拦截器与跨域
创建一个全局配置类 GlobalWebMvcConfig,配置跨域并注册认证拦截器:
@Slf4j
@Configuration
public class GlobalWebMvcConfig implements WebMvcConfigurer {
/**
* 配置跨域
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS", "HEAD")
.allowedHeaders("*")
.exposedHeaders("Server", "Content-Length", "Authorization", "Access-Token", "Access-Control-Allow-Origin", "Access-Control-Allow-Credentials");
}
/**
* 添加拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/**").excludePathPatterns("/static/**");
}
}
第五步:实现认证拦截器
创建 AuthenticationInterceptor 拦截器,在请求到达控制器前验证 JWT:
@Slf4j
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从 http 请求头中取出 token
final String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
// 如果是方法探测(OPTIONS请求),直接通过
if (HttpMethod.OPTIONS.equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
// 如果方法有 JwtIgnore 注解,直接通过
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
if (method.isAnnotationPresent(JwtIgnore.class)) {
JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class);
if (jwtIgnore.value()) {
return true;
}
}
// 判断 token 不为空
LocalAssert.isStringEmpty(token, "token为空,认证失败!");
// 验证 token 并获取 token 内部信息
String userToken = JwtTokenUtil.verifyToken(token);
// 将 token 放入本地线程上下文
WebContextUtil.setUserToken(userToken);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 方法结束后,移除缓存的 token
WebContextUtil.removeUserToken();
}
}
对于不熟悉拦截器的朋友,可以参考这篇文章:Spring Boot拦截器详解。
第六步:创建登录接口
在 Controller 中实现登录逻辑,成功验证后生成 JWT 并放入响应头:
/**
* 登录
* @param userDto
* @return
*/
@JwtIgnore
@RequestMapping(value = "/login", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
public void login(@RequestBody UserDto userDto, HttpServletResponse response) {
//... 参数合法性验证
// 从数据库获取用户信息
User dbUser = userService.selectByUserId(userDto.getUserId);
//... 用户名和密码验证
// 创建 token 并将 token 放入响应头
UserToken userToken = new UserToken();
userToken.setUserId(dbUser.getUserId);
userToken.setUserEmail(dbUser.getUserEmail);
userToken.setUserName(dbUser.getUserName);
String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken));
response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, token);
}
辅助工具类
@JwtIgnore 注解:用于标记无需Token验证的方法(如登录、获取验证码接口)。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtIgnore {
boolean value() default true;
}
WebContextUtil 工具类:利用 ThreadLocal 在请求线程内存储和获取已解析的用户信息。
public class WebContextUtil {
// token 本地线程缓存
private static ThreadLocal<String> local = new ThreadLocal<>();
/**
* 设置 token 信息
* @param content
*/
public static void setUserToken(String content) {
removeUserToken();
local.set(content);
}
/**
* 获取 token 信息
* @return
*/
public static UserToken getUserToken() {
if (local.get() != null) {
UserToken userToken = JSONObject.parseObject(local.get(), UserToken.class);
return userToken;
}
return null;
}
/**
* 移除 token 信息
* @return
*/
public static void removeUserToken() {
if (local.get() != null) {
local.remove();
}
}
}
测试验证
启动项目后,使用 Postman 等工具调用登录接口:

可以看到,响应头中已经包含了生成的 JWT(Jwt 字段)。当需要请求其他受保护的接口时,只需在请求头的 Headers 中添加相同的 Jwt 参数即可通过拦截器的验证。
通过以上步骤,我们就实现了一个基于 Spring Boot 和 JWT 的无状态身份认证系统。它有效地解决了分布式环境下的 Session 共享难题,简化了架构,是构建现代微服务和 API 服务的优选方案。如果你在实践过程中遇到问题,欢迎在云栈社区的技术论坛与其他开发者交流探讨。