JWT
JWT(JSON Web Token) 是基于 RFC 7519 标准的轻量级、自包含的开放数据传输标准,主要用于网络应用中的身份认证与安全信息交换。其核心优势是无状态、自包含、可跨域、易扩展,特别适合前后端分离、微服务与分布式架构。
一、JWT 结构
JWT 是一段由 . 分隔的 Base64URL 编码字符串,格式固定为三部分:
Header.Payload.Signature
示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1. Header(头部)
-
作用:描述令牌元数据(类型、签名算法)
-
格式:JSON → Base64URL 编码
-
典型字段
alg:签名算法(如HS256、RS256、ES256)typ:固定为JWT
示例:
{
"alg": "HS256",
"typ": "JWT"
}
2. Payload(载荷 / 声明)
- 作用:存放实际业务数据(用户 ID、权限、过期时间等)
- 格式:JSON → Base64URL 编码
- ⚠️ 注意:仅编码、未加密,严禁存放密码、手机号等敏感明文
三类声明(Claims) :
-
标准注册声明(推荐)
iss:签发者(如服务域名)sub:主题(通常是用户 ID)aud:受众(接收方)exp:过期时间(Unix 时间戳,必须大于签发时间)nbf:在此时间前不可用iat:签发时间jti:JWT ID(唯一标识,防重放)
-
公共声明
- 自定义公开字段(如
name,role,scope)
- 自定义公开字段(如
-
私有声明
- 通信双方约定的私有字段
示例:
{
"sub": "1234567890",
"name": "John Doe",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
3. Signature(签名)
- 作用:验证 Header+Payload 未被篡改、确保发送方真实可信
- 生成公式:
Signature = HMAC-SHA256( Base64URL(Header) + "." + Base64URL(Payload), 密钥/私钥 )
-
常见算法
- HS256(对称):单密钥签名 + 验证,简单高效
- RS256(非对称):私钥签名、公钥验证,更安全
- ES256(椭圆曲线):短密钥、高安全性
二、JWT 认证流程(典型场景)
- 用户登录:客户端提交用户名 / 密码
- 服务端验证:验证账号密码 → 生成 JWT
- 返回令牌:服务端将 JWT 发给客户端
- 客户端存储:存在 localStorage/sessionStorage/cookie
- 后续请求:
- 放在 HTTP Header:
Authorization: Bearer <token> - 或 POST 参数、URL 参数
- 放在 HTTP Header:
- 服务端验证
- 拆分 Header.Payload.Signature
- 用相同算法 / 密钥重新计算签名
- 对比签名是否一致、检查
exp是否过期
- 通过则处理请求
三、核心特点
- 自包含:令牌自带所有必要信息,无需查会话库
- 无状态:服务端不存会话状态,水平扩展极方便
- 跨域 / 跨服务:可在多域名、微服务间传递
- 紧凑高效:体积小,可在 URL/Header/POST 中传输
- 语言无关:支持几乎所有主流语言
四、优缺点
✅ 优点
- 分布式 / 微服务友好(无状态、易共享)
- 减少数据库 / Redis 查询(自包含)
- 跨域、跨平台、跨技术栈通用
- 易于实现单点登录(SSO)
❌ 缺点
- 令牌不可主动作废(除非引入黑名单 / Redis)
- Payload 仅编码不加密,敏感信息不能明文放
- 体积比 SessionID 大,占用少量更多带宽
- 需严格控制过期时间(
exp)
五、常见应用场景
- 前后端分离项目(Vue/React/ 小程序)身份认证
- API 接口授权(开放平台、第三方调用)
- 微服务内部鉴权、网关认证
- 单点登录(SSO)
- 一次性短时效授权(密码重置、邮箱验证)
六、安全最佳实践
- 必须使用 HTTPS(防止中间人截获 Token)
- 短过期时间(access_token:15~60 分钟)
- 双令牌机制
- Access Token:短时效,用于业务请求
- Refresh Token:较长时效,仅用于刷新 Access Token
- 敏感信息不放 Payload(如密码、身份证)
- 密钥高强度、定期轮换
- 关键操作二次验证(支付、改密)
- 必要时实现令牌黑名单(登出 / 改密后加入黑名单)
七、JWT vs Session-Cookie(对比)
表格
| 特性 | JWT | Session-Cookie |
|---|---|---|
| 状态 | 无状态(服务端不存) | 有状态(服务端存 Session) |
| 分布式 | 天然支持 | 需共享 Session(Redis) |
| 跨域 | 良好 | 受 Cookie 跨域限制 |
| 存储位置 | 客户端(localStorage/cookie) | 服务端(内存 / Redis) |
| 主动失效 | 难(需黑名单) | 易(服务端直接删除) |
| 带宽 | 较大(Token 较长) | 小(仅 SessionID) |
总结
JWT 是现代无状态认证的主流方案,以 Header.Payload.Signature 结构实现安全、自包含、可跨域的身份与授权传递。核心是签名防篡改、自包含、无状态。使用时务必:HTTPS + 短过期 + 双令牌 + 不存敏感明文。
对比JWT和传统的基于Session的认证方式的优缺点
下面用最清晰、最直白、面试常考的方式,对比 JWT 认证 和 传统 Session-Cookie 认证 的优缺点。
一、核心原理一句话总结
- Session-Cookie
客户端只存一个SessionID,服务端在内存 / Redis 里存用户完整会话信息。
→ 有状态认证 - JWT
令牌本身包含用户信息,服务端不存会话,只通过签名验证合法性。
→ 无状态认证
二、详细优缺点对比
1. 服务端状态
Session
- 优点:服务端完全控制会话,可随时销毁、踢人、修改权限
- 缺点:有状态,分布式环境必须共享 Session(Redis)
JWT
- 优点:无状态,服务端不存任何信息,水平扩展极其方便
- 缺点:令牌签发后无法主动作废,想踢人必须加黑名单
2. 分布式 / 微服务支持
Session
- 优点:简单、成熟
- 缺点:多服务器必须共享 Session,架构复杂
JWT
- 优点:天生支持分布式、微服务、跨域
- 缺点:无
3. 跨域与前后端分离
Session
- 优点:传统 Web 项目非常稳定
- 缺点:Cookie 跨域限制大,不适合前后端分离、小程序、APP
JWT
- 优点:完美支持跨域、APP、小程序、第三方 API
- 缺点:前端需要手动存储并携带令牌
4. 性能与请求大小
Session
- 优点:Cookie 很小,请求开销低
- 缺点:每次请求都要查 Redis / 数据库
JWT
- 优点:不用查库,验证极快
- 缺点:令牌较长,Header 占用带宽稍大
5. 安全性
Session
- 优点:信息存在服务端,不容易泄露
- 缺点:依赖 Cookie,易受 CSRF 攻击
JWT
- 优点:不依赖 Cookie,天然防 CSRF
- 缺点:Payload 只是编码不是加密,不能放敏感信息
- 一旦泄露,别人就能直接用(除非短过期 + HTTPS)
6. 令牌失效与管理
Session
- 优点:想踢人就删 Session,实时生效
- 缺点:无
JWT
- 优点:无
- 缺点:无法主动失效,必须靠过期或黑名单
7. 适用场景
Session
- 传统 Web 网站
- 单域名系统
- 需要频繁踢人、强制下线的后台系统
JWT
- 前后端分离(Vue/React)
- APP、小程序
- 微服务、API 开放平台
- SSO 单点登录
三、优缺点总结表(最精简版)
表格
| 对比项 | Session-Cookie | JWT |
|---|---|---|
| 状态 | 有状态,服务端存储 | 无状态,不存储 |
| 分布式支持 | 差,需共享 Session | 极好,天然支持 |
| 跨域 | 受 Cookie 限制 | 不受限,适合前后端分离 |
| 主动失效 | 容易,随时踢人 | 困难,需过期或黑名单 |
| 性能 | 需查库,略慢 | 验证快,无需查库 |
| 安全风险 | CSRF | 令牌泄露风险,不能存敏感明文 |
| 实现复杂度 | 简单,框架内置 | 稍复杂,需处理令牌存储、刷新 |
四、一句话结论
- 传统网站、后台管理系统 → 用 Session 更简单安全
- 前后端分离、APP、小程序、微服务、API 授权 → 用 JWT 更合适
JWT 与 Session 认证流程对比图
我用纯文本流程图给你画最清晰、最直观的版本,一眼看懂核心差异!
一、传统 Session-Cookie 认证流程
[客户端] 提交用户名+密码 [服务端]
│ ───────────────────────────────► │
│ │
│ │ 验证账号密码
│ │ 生成 Session 存储用户信息
│ │ 生成 SessionID
│ ◄─────────────────────────────── │
│ 返回 SessionID │
│
│ 浏览器自动保存到 Cookie
│
│ 后续请求(自动携带 Cookie)
│ ───────────────────────────────► │
│ │
│ │ 根据 SessionID 查询 Session
│ │ 验证通过 → 处理请求
│ │
│ ◄─────────────────────────────── │
│ 响应数据 │
核心特点
- 服务端存数据,客户端只存一个 ID
- 有状态,必须查会话库
- 浏览器自动带 Cookie
二、JWT 认证流程
[客户端] 提交用户名+密码 [服务端]
│ ───────────────────────────────► │
│ │
│ │ 验证账号密码
│ │ 生成 JWT(Header.Payload.Signature)
│ ◄─────────────────────────────── │
│ 返回 JWT │
│
│ 手动保存到 localStorage/Cookie
│
│ 后续请求(手动放在 Header)
│ Authorization: Bearer <JWT>
│ ───────────────────────────────► │
│ │
│ │ 验证 JWT 签名 + 过期时间
│ │ 无需查库 → 直接解析用户信息
│ │
│ ◄─────────────────────────────── │
│ 响应数据 │
核心特点
- 服务端不存任何数据
- 无状态,解析即认证
- 客户端手动带令牌
三、一眼看懂最大区别
Session:服务端存信息 → 客户端拿ID查信息
JWT:服务端不存信息 → 客户端拿令牌直接用
需要我把这两张流程图做成面试可直接背诵的极简版吗?
JWT工具类
package com.zssd.web.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* @author Zhang Gang
* @since 2026-04-08
**/
@SuppressWarnings("unused")
@Slf4j
@Component
public class JwtUtil {
private volatile JwtParser jwtParser;
/**
* 签名密钥(实际使用时应从配置文件读取)
*/
@Value("${jwt.secret}")
private String SECRET_KEY;
/**
* token有效期(毫秒),默认值为1天
*/
@Value("${jwt.expiration}")
private long EXPIRATION;
/**
* token刷新阈值
*/
@Value("${jwt.refreshThreshold}")
private long REFRESH_THRESHOLD_MILLIS;
/**
* 时钟偏差(毫秒)
*/
@Value("${jwt.clock-skew}")
private long CLOCK_SKEW;
/**
* 获取 JWT 签名密钥
* <p>
* 将配置的字符串密钥转换为 JJWT 库要求的 Key 对象。
* 使用 HMAC-SHA 算法生成安全的签名密钥,确保:
* <ul>
* <li>密钥长度符合 HS256 算法要求(至少 256 位)</li>
* <li>使用统一的字符编码转换,避免平台差异</li>
* </ul>
* </p>
*
* @return Key JJWT 签名密钥对象,用于 Token 的签名和验证
*/
private Key getSigningKey() {
return Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
}
/**
* 生成 JWT Token(无自定义声明,使用默认过期时间)
* <p>
* 根据主题(Subject)生成最基础的 Token。
* 不包含额外的自定义声明数据,过期时间采用系统配置的默认值。
* </p>
*
* @param subject 主题,通常为用户名或用户唯一标识
* @return String 生成的 JWT Token 字符串
*/
public String generateToken(String subject) {
Map<String, Object> claims = new HashMap<>();
return generateToken(claims, subject);
}
/**
* 生成 JWT Token(使用默认过期时间)
* <p>
* 根据提供的自定义声明(Claims)和主题(Subject)生成 Token。
* 过期时间将使用配置文件中定义的默认值。
* </p>
*
* @param claims 自定义声明数据,用于在 Token 中携带额外信息
* @param subject 主题,通常为用户名或用户唯一标识
* @return String 生成的 JWT Token 字符串
*/
public String generateToken(Map<String, Object> claims, String subject) {
return createToken(claims, subject, 0);
}
/**
* 生成 JWT Token(核心实现)
* <p>
* 根据自定义声明、主题及过期时间构建并签名 Token。
* 若未指定过期时间,则采用系统配置的默认值。
* </p>
*
* @param claims 自定义声明数据,用于在 Token 中携带额外业务信息
* @param subject 主题,通常为用户名或用户唯一标识
* @param expiration 过期时长(毫秒);若为 null 则使用默认配置
* @return String 生成的 JWT Token 字符串
*/
public String createToken(Map<String, Object> claims, String subject, long expiration) {
Date now = new Date();
Date expirationDate = new Date(now.getTime() + (expiration == 0 ? EXPIRATION : expiration));
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expirationDate)
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
/**
* 解析并提取 Token 中的所有声明(Claims)
* <p>
* 验证 Token 的签名和有效性后,返回包含所有载荷信息的 Claims 对象。
* 可用于获取用户名、过期时间以及任何自定义添加的业务数据。
* </p>
*
* @param token JWT Token 字符串
* @return Claims 包含 Token 所有载荷信息的声明对象
* @throws io.jsonwebtoken.JwtException Token 无效、过期或签名验证失败时抛出
*/
private Claims extractAllClaims(String token) throws JwtException {
return getJwtParser()
.parseClaimsJws(token)
.getBody();
}
/**
* 通用声明提取方法(泛型)
* <p>
* 解析 Token 获取所有 Claims,并通过函数式接口提取指定的字段值。
* 采用泛型设计,支持灵活获取 Subject、Expiration 或自定义业务数据。
* </p>
*
* @param token JWT Token 字符串
* @param claimsResolver 用于从 Claims 中提取特定值的函数式接口
* @param <T> 返回值类型
* @return T 提取出的声明字段值
* @throws io.jsonwebtoken.JwtException Token 无效、过期或签名验证失败时抛出
*/
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) throws JwtException {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
/**
* 从 Token 中提取主题(Subject)
* <p>
* 解析 Token 并获取其中存储的主题信息,通常为用户名或用户唯一标识。
* </p>
*
* @param token JWT Token 字符串
* @return String Token 中的主题字段值
* @throws io.jsonwebtoken.JwtException Token 无效、过期或签名验证失败时抛出
*/
public String getSubject(String token) throws JwtException {
return extractClaim(token, Claims::getSubject);
}
/**
* 检查 Token 是否已过期
* <p>
* 通过对比 Token 中的过期时间与当前系统时间进行判断。
* 若解析过程中捕获到过期异常或其他解析错误,出于安全考虑均视为已过期。
* </p>
*
* @param token JWT Token 字符串
* @return boolean true-已过期或无效,false-未过期
*/
public boolean isTokenExpired(String token) {
try {
Date expiration = extractExpiration(token);
boolean expired = expiration.before(new Date());
if (expired) {
log.debug("Token已过期,过期时间: {}", expiration);
}
return expired;
} catch (ExpiredJwtException e) {
log.debug("Token已过期: {}", e.getMessage());
return true;
} catch (Exception e) {
log.error("检查Token过期状态时发生异常: {}", e.getMessage(), e);
return true;
}
}
/**
* 从 Token 中提取过期时间
* <p>
* 解析 Token 并获取其中设置的有效期截止时间。
* </p>
*
* @param token JWT Token 字符串
* @return Date Token 的过期时间点
* @throws io.jsonwebtoken.JwtException Token 无效、过期或签名验证失败时抛出
*/
public Date extractExpiration(String token) throws JwtException {
return extractClaim(token, Claims::getExpiration);
}
/**
* 获取 Token 的剩余有效时间(毫秒)
* <p>
* 计算 Token 过期时间与当前系统时间的差值。
* 如果 Token 已过期或解析失败,则返回 -1。
* </p>
*
* @param token JWT Token 字符串
* @return long 剩余有效时间(毫秒);若 Token 无效或已过期则返回 -1
*/
public long getRemainingTime(String token) {
try {
Date expiration = extractExpiration(token);
return expiration.getTime() - System.currentTimeMillis();
} catch (ExpiredJwtException e) {
return e.getClaims().getExpiration().getTime() - System.currentTimeMillis();
} catch (Exception e) {
log.error("获取剩余时间失败: {}", e.getMessage());
return -1;
}
}
/**
* 验证 Token 的有效性
* <p>
* 校验 Token 中的用户名是否与预期一致,并检查 Token 是否已过期。
* 同时捕获签名无效、格式错误等异常,确保验证过程的安全性。
* </p>
*
* @param token JWT Token 字符串
* @param username 预期的用户名
* @return Boolean true-验证通过,false-验证失败或 Token 无效
*/
public boolean validateToken(String token, String username) {
try {
final String extractedUsername = getSubject(token);
boolean valid = extractedUsername.equals(username) && !isTokenExpired(token);
if (!valid) {
log.debug("Token验证失败 - 用户名不匹配或Token已过期");
}
return valid;
} catch (ExpiredJwtException e) {
log.debug("Token验证时检测到过期: {}", e.getMessage());
return false;
} catch (SignatureException e) {
log.error("Token签名验证失败: {}", e.getMessage());
return false;
} catch (MalformedJwtException e) {
log.error("Token结构非法导致验证失败: {}", e.getMessage());
return false;
} catch (Exception e) {
log.error("Token验证过程中发生未知异常: {}", e.getMessage());
return false;
}
}
/**
* 检查 Token 是否已过期(别名方法)
* <p>
* 委托给 isTokenExpired 执行实际的过期校验逻辑。
* </p>
*
* @param token JWT Token 字符串
* @return Boolean true-已过期或无效,false-未过期
*/
public boolean isExpired(String token) {
return isTokenExpired(token);
}
/**
* 解析 Token 并获取所有声明(静态方法)
* <p>
* 提供静态入口以便在不注入 Bean 的情况下快速解析 Token。
* 注意:调用此方法前需确保 JwtUtil 已初始化且 SECRET_KEY 已正确配置。
* </p>
*
* @param token JWT Token 字符串
* @return Claims 包含 Token 所有载荷信息的声明对象
* @throws io.jsonwebtoken.JwtException Token 无效、过期或签名验证失败时抛出
* @throws ExpiredJwtException 如果Token已过期
*/
public Claims parseToken(String token) {
return extractAllClaims(token);
}
/**
* 安全解析 Token 并获取声明(静态方法)
* <p>
* 尝试解析 Token,针对不同类型的异常采取差异化处理:
* <ul>
* <li>若 Token 仅过期,仍返回其 Claims 信息以便业务层识别用户身份</li>
* <li>若签名无效、格式错误或发生其他异常,则记录日志并返回 null</li>
* </ul>
* </p>
*
* @param token JWT Token 字符串
* @return Claims Token 的载荷信息;若 Token 非法或解析严重失败则返回 null
*/
public Claims parseTokenSafely(String token) {
try {
return extractAllClaims(token);
} catch (ExpiredJwtException e) {
log.warn("Token已过期,但返回过期的Claims信息: {}", e.getMessage());
return e.getClaims();
} catch (SignatureException e) {
log.error("Token签名无效: {}", e.getMessage());
return null;
} catch (MalformedJwtException e) {
log.error("Token格式错误: {}", e.getMessage());
return null;
} catch (Exception e) {
log.error("Token解析失败: {}", e.getMessage());
return null;
}
}
/**
* 根据剩余有效期判断并刷新 Token
* <p>
* 检查 Token 的剩余时间是否低于指定的阈值。
* 若低于阈值,则提取原有声明(移除过期相关字段)重新生成新 Token;
* 若仍有效且未达阈值,则返回原 Token。
* </p>
*
* @param token 原始 JWT Token 字符串
* @param refreshThresholdMillis 触发刷新的剩余时间阈值(毫秒)
* @return String 新生成的 Token 或原 Token;若解析失败则返回 null
*/
public String refreshTokenIfNeeded(String token, long refreshThresholdMillis) {
try {
Claims claims = parseTokenSafely(token);
if (claims == null) {
return null;
}
long remainingTime = claims.getExpiration().getTime() - System.currentTimeMillis();
if (remainingTime <= (refreshThresholdMillis <= 0 ? REFRESH_THRESHOLD_MILLIS : refreshThresholdMillis)) {
// 需要刷新,生成新Token
String subject = claims.getSubject();
// 移除过期时间和签发时间,让新Token重新生成
claims.remove(Claims.EXPIRATION);
claims.remove(Claims.ISSUED_AT);
return generateToken(claims, subject);
}
return token;
} catch (Exception e) {
log.error("刷新Token失败: {}", e.getMessage());
return null;
}
}
/**
* 获取 JWT 解析器实例(线程安全懒加载)
* <p>
* 采用双重检查锁定(DCL)模式确保 JwtParser 只被初始化一次。
* JwtParser 是线程安全的,复用该实例可以避免频繁创建对象带来的性能开销和 GC 压力。
* </p>
* <p>
* 配置时钟容差(Clock Skew)为 60 秒,用于容忍以下场景:
* <ul>
* <li>不同服务器之间的系统时间微小差异</li>
* <li>网络传输延迟导致的验证时间偏差</li>
* <li>客户端与服务器端的时间不同步</li>
* </ul>
* </p>
*
* @return JwtParser 配置好签名密钥和时钟容差的解析器实例
*/
private JwtParser getJwtParser() {
if (jwtParser == null) {
synchronized (JwtUtil.class) {
if (jwtParser == null) {
jwtParser = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.setAllowedClockSkewSeconds(CLOCK_SKEW)
.build();
}
}
}
return jwtParser;
}
}