JWT令牌

JWT令牌

JWT介绍

什么是 JWT

JSON Web Token(JWT,RFC 7519 标准)是一种基于 JSON 格式的轻量级开放标准,用于在网络参与方之间安全传递声明信息,具备紧凑、自包含、可验证的核心属性。

核心特点

  • 紧凑性:JWT 采用 Base64URL 编码压缩数据体积,可高效通过 URL 参数、HTTP 请求头或 POST 载荷传输,适配带宽受限的网络场景,且传输效率优于传统会话标识(Session ID);
  • 自包含性:Token 内部封装了身份认证、权限声明等核心信息,服务端无需查询数据库或缓存即可完成身份校验,大幅降低服务端存储依赖,尤其适配分布式系统架构;
  • 可验证性:JWT 通过数字签名机制保证信息完整性与不可篡改性,支持两种签名方案:
    对称加密(如 HS256):使用同一密钥完成签名与验签,适用于信任度高的内部系统;
    非对称加密(如 RS256):使用私钥签名、公钥验签,适用于跨信任域的多方通信(如微服务间交互)。

核心应用场景

身份认证(替代传统 Session 机制):用户登录后,服务端生成包含用户身份信息的 JWT 并返回,后续请求通过携带 JWT 完成身份校验,无需服务端存储会话状态,天然适配分布式 / 微服务架构下的单点登录场景(如跨站点、跨应用统一登录);

安全信息交换:在信任的参与方之间传递结构化、可验证的业务声明(如微服务间传递用户权限、接口调用凭证),签名机制确保信息在传输过程中未被篡改,部分场景下可对 Token 内容加密(而非仅签名),实现信息机密性保护。

JWT 的核心结构

JWT 的核心结构由 Header(头部)、Payload(载荷)、Signature(签名)三部分组成,各部分独立完成特定功能,经 Base64URL 编码后通过英文点号(.)连接。

Header(头部)

描述 JWT 元数据(声明 Token 类型和签名算法)

原始 JSON Header:

javascript 复制代码
{
  "alg": "HS256",
  "typ": "JWT"     // Token类型,固定为JWT
}

Base64URL 编码后结果为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

格式与内容

  • 本质是 JSON 对象,需满足 JSON 规范(键值对、字符串值等)
  • 必须包含的字段:
    typ:固定值为 JWT,声明当前令牌的类型;
    alg:指定签名算法,常见值包括:
    对称加密:HS256(HMAC-SHA256)、HS384、HS512(单密钥签名 / 验签);
    非对称加密:RS256(RSA-SHA256)、RS384、RS512(私钥签名、公钥验签);
  • 可选字段:如 cty(内容类型,用于嵌套 JWT 时声明)。

编码规则

对 JSON 对象执行 Base64URL 编码(区别于普通 Base64):

替换 + 为 -、/ 为 _(避免 URL/HTTP 头中的特殊字符冲突);移除末尾填充的等号(=),进一步压缩体积。

Payload(载荷)

存储核心声明信息,承载需要在各方之间传递的实际数据,是 JWT 的业务核心

原始 JSON Payload:

javascript 复制代码
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622,
  "role": "admin"
}

Base64URL 编码后结果:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjIsInJvbGUiOiJhZG1pbiJ9

声明类型

按规范约束分类 JWT 定义了三类声明,确保跨系统兼容性:

  • 注册声明(Registered Claims):JWT 标准预定义的、具备通用含义的声明(可选但推荐使用),核心字段包括:
    iss(Issuer):签发者(如服务端域名);
    sub(Subject):主题(通常为用户唯一标识,如用户 ID);
    aud(Audience):受众(JWT 的接收方,如特定应用);
    exp(Expiration Time):过期时间戳(必须是数字,单位秒,过期后 Token 失效,核心安全字段);
    nbf(Not Before):生效时间戳(在此时间前 Token 不可用);
    iat(Issued At):签发时间戳;
    jti(JWT ID):唯一标识(防止重放攻击)。
  • 公有声明(Public Claims):自定义声明,需在 IANA JSON Web Token Registry 中注册,避免字段冲突。
  • 私有声明(Private Claims):各方协商的自定义声明,无规范约束,但需避免与注册 / 公有声明重名。

关键约束

Payload 仅通过 Base64URL 编码,任何人获取 JWT 后均可解码查看内容,因此禁止存储敏感信息(如密码、令牌密钥);

若需保护 Payload 内容,需使用 JWE(JSON Web Encryption)标准对 Payload 加密(而非仅签名的 JWS)。

编码规则

对 JSON 对象执行 Base64URL 编码(无填充、字符替换)

Signature(签名)

通过加密算法生成签名,验证 JWT 未被篡改(完整性)且由合法签发者生成(真实性),是 JWT 安全的核心保障。

生成逻辑

  1. 拼接编码后的 Header 和 Payload:encodedHeader + "." + encodedPayload
  2. 使用 Header 中指定的算法(alg),结合密钥对拼接字符串进行签名:
    对称加密(HS256 示例):
    签名公式 = HMACSHA256(encodedHeader + "." + encodedPayload, secretKey)
    (secretKey 为服务端与客户端共享的密钥,需严格保密)
    非对称加密(RS256 示例):
    签名公式 = RSASHA256(encodedHeader + "." + encodedPayload, privateKey)
    (privateKey 为签发方私钥,验签时使用对应公钥);
  3. 对签名结果执行 Base64URL 编码,得到最终的 Signature 部分。

验签过程

服务端接收 JWT 后:

拆分出 Header、Payload、Signature 三部分;

解码 Header 并获取签名算法;

按相同规则拼接 encodedHeader + "." + encodedPayload,使用对应密钥(对称加密用共享密钥,非对称加密用公钥)执行相同算法生成验证签名;

对比 "验证签名" 与 JWT 中的 Signature:若一致,则 Token 未被篡改且签发合法;若不一致,则 Token 无效。

示例(HS256 签名)

拼接字符串:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjIsInJvbGUiOiJhZG1pbiJ9 使用密钥 your-256-bit-secret 执行 HMACSHA256 签名并 Base64URL 编码后,得到 Signature:dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Java 实操

生成 JWT Token

JJWT 是 Java 生态中最常用的 JWT 工具库,基于 JJWT 库生成 JWT 登录令牌

引入依赖(Maven)

xml 复制代码
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>>0.9.1</version>
</dependency>

jjwt:0.9.1 是 JJWT 早期的单体包,把核心接口、算法实现、JSON 序列化(依赖 Jackson)、异常处理等所有逻辑打包在一个 JAR 里。

引入后会一次性加载所有功能,即使只用到 HS256 签名,也会引入 RS256/ES256 等未使用的算法实现。

xml 复制代码
<!-- JJWT核心 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<!-- 实现层 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<!-- 算法支持 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

JJWT 从 0.10.x 开始重构为模块化架构,拆分出三个核心依赖:

jjwt-api:核心接口层,仅定义规范,无具体实现,作为编译期依赖(compile 范围);

jjwt-impl:接口的默认运行时实现,仅在运行时生效(runtime 范围),编译期不耦合;

jjwt-jackson:JSON 序列化实现(依赖 Jackson),同样为运行时依赖,可替换为 jjwt-gson/jjwt-orgjson 适配不同序列化框架。

这种设计符合接口与实现分离的原则,便于扩展和维护。

java 复制代码
public class JwtUtil {
    // 密钥(HS256要求密钥长度≥256位)
    private static final String SECRET_KEY = "my_256bit_secret_key_123456789012345678901234";
    // Token过期时间单位是毫秒
    private static final long EXPIRATION_TIME = 3600 * 1000;

    // 生成JWT
    public static String generateToken(String userId, String username) {
        // 1. 构建Header
        Map<String, Object> header = new HashMap<>();
        header.put("alg", "HS256");
        header.put("typ", "JWT");

        // 2. 构建Payload
        return Jwts.builder()
                .setHeader(header)                // 设置头部
                .setSubject(userId)               // 注册声明用户ID
                .claim("username", username)      // 自定义声明用户名
                .claim("role", "admin")           // 自定义声明角色
                .setIssuedAt(new Date())          // 注册声明签发时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) // 过期时间
                .signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()), SignatureAlgorithm.HS256) // 签名
                .compact();                       // 生成最终Token
    }

    // 测试
    public static void main(String[] args) {
        // 生成Token
        String token = generateToken("123456", "张三");
        System.out.println("生成的JWT:" + token);
    }
}

Jwts.builder() 是核心入口,用于创建 JJWT 库的 JWT 构建器对象;

setHeader(header) 对应 JWT 的 Header,这里使用的是 HS256 对称加密;

setSubject(userId) 对应 Payload 中的注册声明(Registered Claim),设置sub字段,值为用户 ID;

setIssuedAt(new Date()) 对应 Payload 中的注册声明,设置iat字段(Issued At),值为当前系统时间;

setExpiration(...) 对应 Payload 中的注册声明,设置exp字段(Expiration Time),值为当前时间 + 过期时长;

claim("username", username) / claim("role", "admin") 对应 Payload 中的私有声明(Private Claim);

signWith(...) 对应 JWT 的 Signature:这是保证 JWT 不被篡改的核心步骤:

Keys.hmacShaKeyFor(SECRET_KEY.getBytes()):将你的字符串密钥(SECRET_KEY)转换为 HS256 算法所需的密钥对象(必须是 256 位以上的密钥,否则会报错);

SignatureAlgorithm.HS256:指定使用 HS256 对称加密算法签名,服务端验签时,会用相同的 SECRET_KEY 重新计算签名,对比是否和令牌中的签名一致,不一致则说明令牌被篡改,直接拒绝。

compact()最终组装:将 Header、Payload 分别做 Base64URL 编码,再用 HS256 算法生成签名,最后用.连接三部分,生成最终的 JWT 字符串。

若用非对称加密(RS256),需先生成 RSA 公钥 / 私钥,替换签名逻辑:

java 复制代码
// 加载RSA私钥(生成签名)
PrivateKey privateKey = Keys.privateKeyFor(SignatureAlgorithm.RS256, Files.readAllBytes(Paths.get("private.key")));
// 加载RSA公钥(验证签名)
PublicKey publicKey = Keys.publicKeyFor(SignatureAlgorithm.RS256, Files.readAllBytes(Paths.get("public.key")));

// 生成Token(私钥签名)
Jwts.builder()
    .setSubject("123456")
    .signWith(privateKey, SignatureAlgorithm.RS256)
    .compact();

// 验证Token(公钥验证)
Jwts.parserBuilder()
    .setSigningKey(publicKey)
    .build()
    .parseClaimsJws(token);

基于 JJWT 库封装的更模块化的 JWT Token 生成逻辑:

先构建 Claims 对象,通过 Claims 对象构建 token

java 复制代码
public class JwtUtil {

    private static final String SECRET_KEY = "my_256bit_secret_key_123456789012345678901234";
    private static final long EXPIRATION_TIME = 3600 * 1000;

    /**
     * 构建JWT载荷(Payload),构建 Claims 对象
     */
    private static Claims buildClaims(String userId, String username) {
        // 入参校验
        if (userId == null || username == null) {
            throw new IllegalArgumentException("userId和username不能为空");
        }

        // 构建Claims对象(使用DefaultClaims实现类)
        Claims claims = new DefaultClaims();
        claims.setSubject(userId);
        claims.put("username", username);
        claims.put("role", "admin");
        claims.setIssuedAt(new Date());
        claims.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME));

        return claims;
    }

    /**
     * 生成 JWT Token(先构建载荷Claims,再生成Token)
     */
    public static String generateToken(String userId, String username) {
        // 先构建载荷Claims
        Claims claims = buildClaims(userId, username);

        // 生成安全的签名密钥
        JwsHeader header = Jwts.header()
                .add("alg", "HS256")
                .add("typ", "JWT")
                .build();

        // 通过Claims构建最终Token
        return Jwts.builder()
                .setHeader(header)                // 设置Header
                .setClaims(claims)                // 绑定预构建的Claims载荷
                .signWith(                        // 签名(新版JJWT兼容写法)
                        Keys.hmacShaKeyFor(SECRET_KEY.getBytes()),
                        SignatureAlgorithm.HS256
                )
                .compact();                       // 生成最终Token字符串
    }
}

解析 JWT Token

解析的本质是反向验证生成 Token 的过程

生成时是「构建 Header/Payload→签名→拼接」,解析时是「拆分→验签→解码→校验→提取」

java 复制代码
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtUtil {
    // 验证并解析JWT
    public static Claims parseToken(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes())) // 设置验证密钥
                    .build()    // 构建JWT解析器对象
                    .parseClaimsJws(token) // 解析Token
                    .getBody(); // 获取Payload
        } catch (ExpiredJwtException e) {
            throw new RuntimeException("Token已过期");
        } catch (UnsupportedJwtException e) {
            throw new RuntimeException("不支持的JWT格式");
        } catch (MalformedJwtException e) {
            throw new RuntimeException("JWT格式错误");
        } catch (SignatureException e) {
            throw new RuntimeException("JWT签名验证失败");
        } catch (IllegalArgumentException e) {
            throw new RuntimeException("JWT为空或无效");
        }
    }

    // 测试
    public static void main(String[] args) {
        // 解析Token
        Claims claims = parseToken(token);
        System.out.println("用户ID:" + claims.getSubject());
        System.out.println("用户名:" + claims.get("username"));
        System.out.println("过期时间:" + claims.getExpiration());
    }
}

Jwts.parserBuilder() 用于初始化 JWT解析器构建器;

setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes())) 设置验签密钥;

build() 根据前面配置的验签密钥构建出最终的 JWT 解析器对象(JwtParser),此时解析器已具备验证能力;

parseClaimsJws(token) 解析传入的 JWT 字符串(token),并自动完成多维度的合法性校验,只有全部校验通过才会继续,否则直接抛出异常。

校验通过后,返回 Jws<Claims> 对象,这个对象包含了解析后的完整 JWT 数据:

getHeader() 获取 JWT 的 Header 部分,getBody() 获取 JWT 的 Payload 部分(Claims 类型的对象),getSignature() 获取 JWT 的 Signature 部分。

getSubject() 是 Claims 类为标准注册声明 sub 提供的快捷方法,等价于 claims.get("sub");

getExpiration() 是 Claims 为标准注册声明 exp 提供的快捷方法,返回 Date 类型的过期时间(等价于 new Date((Long) claims.get("exp"))),直接打印会输出 Date 的默认字符串格式(如 Tue Dec 23 16:30:00 CST 2025);

从 Claims 对象中提取自定义私有声明的值 Claims 没有专门的快捷方法,所以用通用的 get(String key) 方法,传入声明的键名即可获取对应值,注意返回值是 Object 类型,若需要特定类型,建议用重载方法如 claims.get("username", String.class),避免后续类型转换问题。

异常捕获:

UnsupportedJwtException:当 token 不是通过 Claims 对象构建的 token 时;

ExpiredJwtException:当 token 已过期时;

MalformedJwtException:当 token 不是有效的 Claims 对象构建的 token 时;

SignatureException:当 token 的 Signature 验证失败时;

IllegalArgumentException:当 token 为 null 或 token 是空字符串或 token 中只有空字符时;

配置JWT的工具类

基于 JJWT 库封装的、适配 Spring 业务场景的业务级工具类

java 复制代码
@Component
public class JwtTokenUtils
{
    private static final String CLAIM_KEY_USERNAME = "user";
    private static final String CLAIM_KEY_CREATED = "created";
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;

    /**
     * 工具方法:用于生成过期时间
     * */
    private Date generateExpirationDate()
    {
        return new Date(System.currentTimeMillis() + expiration*1000);
    }
    /**
     * 工具方法:用于通过token获取荷载
     */
    private Claims getClaimsFormToken(String token)
    {
        Claims claims = null;
        try
        {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        return claims;
    }
    /**
     * 工具方法:用于通过token获取失效时间
     * */
    private Date getExpiredTimeFromToken(String token)
    {
        Claims claims = getClaimsFormToken(token);
        return claims.getExpiration();
    }
    /**
     * 工具方法:用于判断token是否达到失效时间
     * 1)如果过期时间早于当前时间,说明token已失效
     * 2)如果过期时间晚于当前时间,说明token未失效
     * */
    private Boolean isTokenExpired(String token)
    {
        Date expirted = getExpiredTimeFromToken(token);
        return expirted.before(new Date());
    }
    /**
     * 1.根据信息生成token
     * */
    public String generateToken(UserDetails details)
    {
        /*第一步:构建荷载用于存放token的容器*/
        Map<String, Object> claim = new HashMap<>();
        claim.put(CLAIM_KEY_USERNAME, details.getUsername());
        claim.put(CLAIM_KEY_CREATED, new Date());
        /**
         * 第二步:通过荷载构建token,并返回
         * */
        return Jwts.builder()
                .setClaims(claim)//设置荷载
                .setExpiration(generateExpirationDate())//设置过期时间
                .signWith(SignatureAlgorithm.HS512, secret)//设置签名算法
                .compact();//该方法用于生成token
    }

    /**
     * 2.外界调用方法:根据token获取用户名
     */
    public String getUserNameFromToken(String token)
    {
        /*
        * 第一步:从token中获取荷载
        * */
        String username = null;
        try
        {
            Claims claims = getClaimsFormToken(token);
            username = claims.getSubject();
        }catch (Exception e)
        {
            username = null;
        }

        return username;
    }
    /**
     * 3.外界调用方法:判断token是否有效。
     * */
    public Boolean validateToken(String token, UserDetails userDetails)
    {
        return getUserNameFromToken(token).equals(userDetails.getUsername())  && !isTokenExpired(token);
    }
}

前端存储token的方式

token存储在了cookie与请求头中:

后端打印所有请求头信息:

java 复制代码
@RestController
public class LoginController {

    @GetMapping("/checkToken")
    public Result login(HttpServletRequest request) {

        System.out.println("======= 请求头(Headers) =======");
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            String headerValue = request.getHeader(headerName);
            System.out.println(headerName + ": " + headerValue);
        }
}
相关推荐
毕设十刻2 小时前
基于Vue的新生入学报道管理系统(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
南山安2 小时前
LangChain 入门实战:从零搭建 AI 应用工作流
javascript·面试·langchain
计算机毕设指导62 小时前
基于微信小程序的派出所业务管理系统【源码文末联系】
java·spring boot·mysql·微信小程序·小程序·tomcat·uniapp
星月心城2 小时前
八股文-JavaScript(第二天)
开发语言·javascript·ecmascript
番茄撒旦在上2 小时前
Docker部署springboot项目
服务器·spring boot·docker·容器
JZXStudio2 小时前
Swift 6 + MLX + SwiftUI:三位一体本地AI架构蓝图
前端·ios
Aevget2 小时前
DevExpress JS & ASP.NET Core v25.1新版亮点 - 新增AI文本编辑功能
javascript·人工智能·asp.net·界面控件·devexpress·ui开发
神秘的猪头2 小时前
彻底搞懂 React 组件通信:从 TodoList 实战出发,解锁 React 开发的“核心姿势” 🚀
前端·react.js·架构
xiaoxue..2 小时前
爬楼梯问题:从递归到动态规划再到闭包的进化之路
javascript·算法·面试·动态规划