JSON Web Token

7 阅读14分钟

JWT

JWT(JSON Web Token) 是基于 RFC 7519 标准的轻量级、自包含的开放数据传输标准,主要用于网络应用中的身份认证与安全信息交换。其核心优势是无状态、自包含、可跨域、易扩展,特别适合前后端分离、微服务与分布式架构。

一、JWT 结构

JWT 是一段由 . 分隔的 Base64URL 编码字符串,格式固定为三部分:

Header.Payload.Signature

示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1. Header(头部)
  • 作用:描述令牌元数据(类型、签名算法)

  • 格式:JSON → Base64URL 编码

  • 典型字段

    • alg:签名算法(如 HS256RS256ES256
    • typ:固定为 JWT

示例:

{
  "alg": "HS256",
  "typ": "JWT" 
}
2. Payload(载荷 / 声明)
  • 作用:存放实际业务数据(用户 ID、权限、过期时间等)
  • 格式:JSON → Base64URL 编码
  • ⚠️ 注意仅编码、未加密严禁存放密码、手机号等敏感明文

三类声明(Claims)

  1. 标准注册声明(推荐)

    • iss:签发者(如服务域名)
    • sub:主题(通常是用户 ID)
    • aud:受众(接收方)
    • exp:过期时间(Unix 时间戳,必须大于签发时间)
    • nbf:在此时间前不可用
    • iat:签发时间
    • jti:JWT ID(唯一标识,防重放)
  2. 公共声明

    • 自定义公开字段(如 name, role, scope
  3. 私有声明

    • 通信双方约定的私有字段

示例:

{
    "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 认证流程(典型场景)

  1. 用户登录:客户端提交用户名 / 密码
  2. 服务端验证:验证账号密码 → 生成 JWT
  3. 返回令牌:服务端将 JWT 发给客户端
  4. 客户端存储:存在 localStorage/sessionStorage/cookie
  5. 后续请求
    • 放在 HTTP Header:Authorization: Bearer <token>
    • 或 POST 参数、URL 参数
  6. 服务端验证
    • 拆分 Header.Payload.Signature
    • 用相同算法 / 密钥重新计算签名
    • 对比签名是否一致、检查 exp 是否过期
  7. 通过则处理请求

三、核心特点

  • 自包含:令牌自带所有必要信息,无需查会话库
  • 无状态:服务端不存会话状态,水平扩展极方便
  • 跨域 / 跨服务:可在多域名、微服务间传递
  • 紧凑高效:体积小,可在 URL/Header/POST 中传输
  • 语言无关:支持几乎所有主流语言

四、优缺点

✅ 优点
  • 分布式 / 微服务友好(无状态、易共享)
  • 减少数据库 / Redis 查询(自包含)
  • 跨域、跨平台、跨技术栈通用
  • 易于实现单点登录(SSO)
❌ 缺点
  • 令牌不可主动作废(除非引入黑名单 / Redis)
  • Payload 仅编码不加密,敏感信息不能明文放
  • 体积比 SessionID 大,占用少量更多带宽
  • 需严格控制过期时间(exp

五、常见应用场景

  • 前后端分离项目(Vue/React/ 小程序)身份认证
  • API 接口授权(开放平台、第三方调用)
  • 微服务内部鉴权、网关认证
  • 单点登录(SSO)
  • 一次性短时效授权(密码重置、邮箱验证)

六、安全最佳实践

  1. 必须使用 HTTPS(防止中间人截获 Token)
  2. 短过期时间(access_token:15~60 分钟)
  3. 双令牌机制
    • Access Token:短时效,用于业务请求
    • Refresh Token:较长时效,仅用于刷新 Access Token
  4. 敏感信息不放 Payload(如密码、身份证)
  5. 密钥高强度、定期轮换
  6. 关键操作二次验证(支付、改密)
  7. 必要时实现令牌黑名单(登出 / 改密后加入黑名单)

七、JWT vs Session-Cookie(对比)

表格

特性JWTSession-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-CookieJWT
状态有状态,服务端存储无状态,不存储
分布式支持差,需共享 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;
    }

}