JWT令牌技术

一、JWT 是什么?

JWT(JSON Web Token)是一种轻量级的、自包含的令牌格式,用于在双方之间安全地传输 JSON 格式的信息。它的核心特点是:

  • 自包含:令牌本身包含了所有必要的用户信息(如用户 ID、角色),无需后端查库就能验证身份;
  • 可验证:通过数字签名保证信息未被篡改,可快速验证合法性;
  • 跨平台:基于 JSON 和 Base64URL,兼容所有编程语言和系统。

二、JWT 的核心结构(三段式)

JWT 由 . 分隔的三部分组成,格式为:Header.Payload.Signature,每部分都是 Base64URL 编码(可逆,非加密)。

部分作用示例(Base64URL 解码后)
Header声明令牌类型和签名算法{"alg":"HS256","typ":"JWT"}
Payload存储业务数据(Claims),仅存非敏感信息{"userId":1001,"username":"test","exp":1710990400}
Signature签名(防篡改),用 Header 声明的算法 + 密钥对 Header+Payload 计算得到二进制哈希值转 Base64URL 后的字符串

关键提醒:

  • Payload 不要存密码、手机号等敏感信息(Base64URL 可解码);
  • exp(过期时间)是必加的 Claim,避免令牌永久有效。

三、JWT中的Signature是如何生成的?

JWT 签名的核心是「哈希算法 + 密钥」,主流算法有两类:

算法类型代表算法特点
对称加密HS256(HMAC-SHA256)签发和验证用同一个密钥,简单易实现
非对称加密RS256(RSA-SHA256)签发用私钥,验证用公钥,更安全(推荐)

我们以HS256(HMAC-SHA256)为例讲解。

HS256 全称是 HMAC-SHA256,是一种基于哈希的消息认证码算法,也是 JWT 中最常用的签名算法:

  • HMAC:带密钥的哈希算法,核心是「哈希算法 + 密钥」,没有密钥就无法生成 / 验证正确的签名;
  • SHA256:哈希算法,把任意长度的数据转换成 256 位(32 字节)的二进制哈希值,不可逆;
  • 对称加密特性:签发(生成签名)和验证(校验签名)使用同一个密钥,这是 HS256 最核心的特点。
1. HS256 签发 Signature 的完整步骤

签发就是 “生成 JWT 签名” 的过程,核心是对「编码后的 Header + 编码后的 Payload」用密钥做 HMAC-SHA256 运算,具体分 5 步:

步骤 1:定义 Header 和 Payload(原始数据)

Header 固定声明算法类型,Payload 存非敏感业务数据:

// Header:声明算法和令牌类型
{
  "alg": "HS256",
  "typ": "JWT"
}

// Payload:业务数据(绝对不存密码!)
{
  "userId": 1001,
  "username": "test",
  "exp": 1710990400  // 过期时间戳
}

步骤 2:对 Header/Payload 做 Base64URL 编码

Base64URL 是 Base64 的 URL 安全变种(替换 +-/_,去掉末尾 =),目的是让数据能在 URL/HTTP 头中安全传输:

  • Header 编码结果:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  • Payload 编码结果:eyJ1c2VySWQiOjEwMDEsInVzZXJuYW1lIjoidGVzdCIsImV4cCI6MTcxMDk5MDQwMH0
步骤 3:拼接待签名的原始字符串

. 拼接编码后的 Header 和 Payload,得到 “待签名字符串”:

unsignedStr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEwMDEsInVzZXJuYW1lIjoidGVzdCIsImV4cCI6MTcxMDk5MDQwMH0"
步骤 4:用密钥对 unsignedStr 做 HMAC-SHA256 运算

这是生成签名的核心步骤,伪代码逻辑:

// 密钥必须是 256 位(32 字节)以上的随机字符串(安全基础!)
secretKey = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2"

// 核心运算:HMAC-SHA256(密钥, 待签名字符串) → 得到二进制签名
binarySignature = HMAC-SHA256(secretKey, unsignedStr)

注意:这里的密钥是只存储在服务器端的,服务器端所有 JWT 的签发 / 验证共用同一个核心密钥(除非做了多密钥版本管理),这个密钥在服务器初始化时加载(比如从配置中心读取),默认不会频繁变更。

步骤 5:对二进制签名做 Base64URL 编码

把二进制的 binarySignature 转换成 Base64URL 字符串,就是最终的 Signature:

// 示例编码结果(实际是32字节二进制转的Base64URL)
encodedSignature = "5f8k8kZ7a9s8d7f6g5h4j3k2l1m0n9b8v7c6x5s4d3f2g1h0j9k8l7m6n5b4v3c2x1z"

Signature 无 “解码” 一说,它是不可逆哈希值,仅用于对比验证,不存储任何数据;

最终:拼接完整 JWT

JWT = encodedHeader + "." + encodedPayload + "." + encodedSignature
// 示例:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEwMDEsInVzZXJuYW1lIjoidGVzdCIsImV4cCI6MTcxMDk5MDQwMH0.5f8k8kZ7a9s8d7f6g5h4j3k2
2.HS256 验证 Signature 的完整步骤

验证是签发的逆过程,核心是 “重新计算签名并对比”,确保 JWT 未被篡改、签发合法,分 4 步:

步骤 1:拆分接收到的 JWT

把前端传来的 JWT 按 . 拆分成三部分:

plaintext

jwt = "header.payload.signature"
encodedHeader = 第一部分
encodedPayload = 第二部分
receivedSignature = 第三部分
步骤 2:重新拼接 unsignedStr

用拆分出的 encodedHeaderencodedPayload 重新拼接:

plaintext

unsignedStr = encodedHeader + "." + encodedPayload
步骤 3:用相同密钥重新计算签名

和签发时用同一个密钥,对新的 unsignedStr 做 HMAC-SHA256 运算,再转 Base64URL 编码:

plaintext

// 同一密钥
secretKey = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2"

// 重新计算
newBinarySignature = HMAC-SHA256(secretKey, unsignedStr)
newEncodedSignature = Base64URL(newBinarySignature)
步骤 4:对比签名
  • 如果 newEncodedSignature == receivedSignature:验证通过(JWT 合法、未被篡改);
  • 如果不相等:验证失败(JWT 被篡改,或密钥错误,直接拒绝)。

注意:JWT验证时,不会进行密码的验证

  • 密码仅在登录阶段验证身份,验证通过后就退场,不会出现在 JWT 的 Payload 或 Signature 中;
  • Signature 是服务器密钥生成的 “防伪标签”,用于验证 JWT 合法性,和密码无任何关联,也无法 “解码” 出任何内容;
  • JWT 验证的核心是 “令牌是否合法”,而非 “密码是否正确”—— 因为合法令牌本身就代表用户已通过密码验证。

四、为什么JWT验证时,不会进行密码的验证

密码既不会存储在 Payload 中,也绝不会出现在 Signature 里,当用户名和密码在数据库当中被搜索到,验证了用户的身份之后,服务器就会给浏览器签发JWT,之后整个 JWT 生命周期中都不会和密码有关联

  1. 密码的作用范围:仅在用户登录阶段验证身份,验证通过后就彻底 “退场”,不会参与 JWT 的生成和验证;
  2. Signature 的作用:仅用于验证 “JWT 是否由服务器签发、是否被篡改”,和密码无关;
  3. JWT 验证的核心:验证的是 “令牌的合法性”,而非 “密码的正确性”—— 因为登录阶段已经验证过密码了。

密码验证成功,签发JWT流程如下。

五、JWT令牌的安全风险

1.JWT 的编码≠加密,拦截后能看到什么?

JWT 由 3 部分组成(Header.Payload.Signature),默认采用 Base64URL 编码(不是加密):

  1. Header:令牌类型、加密算法(如 HS256)→ 可解码;
  2. Payload:存储的自定义数据(如用户 ID、用户名、过期时间)→ 可解码;
  3. Signature:签名(防止篡改)→ 无法逆向解析。

黑客拦截后,只需把前两段(Header、Payload)单独拿出来做 Base64 解码,就能看到:

  1. Header 解码结果:{"alg":"HS256","typ":"JWT"}
  2. Payload 解码结果:{"userId":1,"name":"admin","exp":1710990400}

👉 结论 1:如果 Payload 里直接存了明文密码,黑客解码后能直接拿到;但规范的 JWT 绝不会在 Payload 里存密码,只会存非敏感的用户标识(如 ID、用户名)。

👉 结论 2:即使 Payload 里没有密码,黑客能看到用户 ID / 用户名等信息,依然有信息泄露风险。

2.黑客拦截 token 后,能做什么?

  • 冒充合法用户:黑客拿着有效的 token,直接在请求头 / Cookie 中携带,就能以你的身份访问系统(直到 token 过期);
  • 信息泄露:解码 Payload 后获取用户 ID、用户名、角色等信息,为进一步攻击提供线索;
  • 尝试篡改 token:比如把userId:1改成userId:2(管理员),但签名验证会失败(除非黑客拿到你的签名密钥)。

六、黑客拦截 JWT 后直接发送给服务端,不就可以冒充用户了?

核心结论:这是 JWT 最大的风险(“令牌冒用”),但可以通过多层防护彻底规避

首先承认:如果黑客能拿到有效的 JWT,并且直接用它发请求,在 token 过期前确实能冒充用户 —— 但这不是 JWT 的设计缺陷,而是传输 / 存储环节的安全问题,我们可以通过以下 5 层防护,让黑客 “拿不到” 或 “拿到也用不了”:

防护 1:传输层加密(最基础)—— 让黑客拿不到 JWT

JWT 只要通过 HTTP 明文传输,黑客就能通过抓包(Fiddler/Wireshark)拦截;必须强制用 HTTPS:

  • HTTPS 会对整个请求(包括 Header/Cookie 中的 JWT)加密,黑客抓包只能拿到密文,无法解析出 JWT;
  • 配合 Cookie 的 Secure 属性:设置 cookie.setSecure(true),让 JWT 仅在 HTTPS 下传输,HTTP 请求不会携带。
防护 2:限制 Cookie 访问权限 —— 让黑客偷不走 JWT

如果 JWT 存在 Cookie 中,必须设置以下属性:

Cookie cookie = new Cookie("token", jwt);
cookie.setHttpOnly(true); // 禁止前端JS读取(防XSS攻击偷取)
cookie.setSameSite("Strict"); // 禁止跨域请求携带(防CSRF攻击)
cookie.setSecure(true); // 仅HTTPS传输
cookie.setMaxAge(30 * 60); // 短过期时间(30分钟)
response.addCookie(cookie);
  • HttpOnly=true:黑客即使注入恶意 JS 脚本,也无法通过 document.cookie 读取 JWT;
  • SameSite=Strict:黑客诱导用户点击恶意链接时,浏览器不会携带该 Cookie,无法冒用。
防护 3:缩短 token 过期时间 + 刷新令牌机制 —— 即使拿到也用不久
  • 访问令牌(Access Token):有效期设为 30 分钟(短),用于接口访问;
  • 刷新令牌(Refresh Token):有效期设为 7 天(长),仅用于获取新的 Access Token;
  • 逻辑:Access Token 过期后,前端用 Refresh Token 换全新的 Access Token;如果 Refresh Token 也过期,用户必须重新登录。
  • 效果:即使黑客拿到 Access Token,最多只能冒用 30 分钟,风险窗口极小。
防护 4:维护 token 黑名单 —— 让被盗的 token 失效

如果发现 token 泄露(比如用户反馈账号异常),后端立即将该 token 加入黑名单(存 Redis,过期时间和 token 一致):

防护 5:传输层加密,强制使用 HTTPS

Cookie 和请求头在 HTTP 协议下是明文传输,黑客可通过抓包(如 Fiddler、Wireshark)直接拦截;HTTPS 会对整个通信过程加密,杜绝抓包获取 token

HTTPS密文传输过程

前置理解:服务器会提前准备一个HTTPS 密钥对(公钥 + 私钥),这个密钥对默认是长期固定的,不会随服务器重启 / 请求变化,但也可以主动更新(只是更新频率极低)。为了方便,我们默认服务器的公钥和私钥保持不变。

  • 客户端(浏览器)向服务器发起 HTTPS 请求,服务器返回公钥证书(包含服务器公钥,可公开);
  • 客户端验证证书合法后,生成一个随机的临时会话密钥(比如 128 位随机字符串);
  • 客户端用服务器的公钥加密这个临时会话密钥,发送给服务器;
  • 服务器用自己的私钥解密,拿到这个临时会话密钥;
  • 后续的所有请求 / 响应,都用这个临时会话密钥做对称加密(一次一密);
  • 会话结束后,临时会话密钥立即销毁,下次请求重新生成。
防护 6:绝对不要在 JWT Payload 中存敏感信息

这是最基础的原则:

  • ❌ 错误做法:{"username":"admin","password":"123456","exp":xxx}
  • ✅ 正确做法:{"userId":1001,"role":"user","exp":1710990400}(仅存非敏感标识)

七、代码实现

1. Java中与JWT技术相关的常见类

  • Jwt接口和类都存在但日常几乎都不使用。Jwt接口是JJWT 库的顶层接口,定义了 JWT 的基础结构;Jwt类是不加密、无签名的JWT令牌。JWT(广义) = JWS(带签名) + JWE(加密) + 纯明文JWT(无签名/加密,不推荐)
  • Jws是带数字签名的 JWT(也是最常用的 JWT 形式),99%的JWT令牌都是Jws的形式
  • Jwe是加密的 JWT,不常见。
  • Jwts是Java JJWT 库中提供的静态工具类(io.jsonwebtoken.Jwts,是操作 JWT(主要是 JWS)的入口。封装了创建 JWT、解析 JWT、验证 JWT 的所有核心方法,相当于操作 JWT 的 “工具箱”。Jwts.claims()创建 Claims 载荷对象;Jwts.parserBuilder()创建解析器(用于解析 / 验证 JWS 令牌);Jwts.builder()创建 JwtBuilder 对象(用于生成 JWS 令牌)
  • JwtBuilder是JJWT 库中的接口,不能直接new,由Jwts.builder()返回,用于一步步构建 JWS 令牌。利用setHeader,setClaims,signWith,toString等方法构建令牌。注意:JJWT 库设计了建造者模式,必须通过Jwts.builder()静态方法获取 JwtBuilder 实例(而非直接 new)
  • JwtParser是JJWT 库中的接口,不能直接new,由Jwts.parseBuilder()返回,用于解析JWT令牌。利用setSigningKey,parseClaimsJws等方法构建令牌。
  • Jws<Claims>解析后的 JWS 令牌对象,封装了 JWS 格式 JWT 的三部分:Header(头)、Body(载荷,即 Claims)、Signature(签名);JwtParser解析后的返回结果,代表 “验证通过的合法令牌”。常见方法有getHeader,getClaims,getSignature
  • Claims是JWT 载荷,作为业务数据载体,存储 JWT 的所有载荷数据是Jws<Claims>的核心内容,业务层几乎所有操作都围绕它展开。包括:标准字段:sub(用户名)、exp(过期时间)、iat(签发时间)、jti(令牌 ID)等;自定义字段:用户 ID、角色、部门等业务数据;

2. 代码
public class JwtUtils{
    private static String privateKey = "123456";

    public static String gernerateJWT(String username){
        //JWT令牌构建
        Map<String,Object> header = new HashMap<>();
        header.put("alg","HMAC-SHA256");
        Map<String,Object> claims = new HashMap<>();
        claims.put("username",username);
        String jwt = Jwts.builder()
                .setHeader(header)
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS256, privateKey)
                .setExpiration(new Date(System.currentTimeMillis() + 24*60*60*1000)) // 设置过期时间为24小时
                .compact();

        return jwt;
    }

    public static Claims parseJWT(String jwt){
        //JWT令牌解析
        Jws<Claims> claims = Jwts.parser().setSigningKey(privateKey).parseClaimsJws(jwt);
        return claims.getBody();
    }


}

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值