【JavaEE】JWT令牌:签名、密钥、生成令牌、校验令牌、编码、解码,一篇文章彻底讲透

老铁们,JWT 令牌用来解决登录问题。可能很多小伙伴有着以下疑问🤔:

  • "那个密钥是干啥的?JWT 里面也没有密钥这个东西啊?"
  • "密钥存在哪?前端能拿到吗?"
  • "多个服务器用同一个密钥还是不同的?"
  • "生成令牌和校验令牌到底干了啥?编码解码又在哪一步?"

别急,那这一篇咱就把 JWT 的密钥、生成、校验、编码、解码 以及多服务器环境下的使用全部唠清楚。


一、JWT 令牌长什么样?三部分各是啥?

一个 JWT 令牌长这样(中间用点分隔):

复制代码
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsImlhdCI6MTY5ODM5Nzg2MCwiZXhwIjoxNjk4Mzk5NjYwfQ.oxup5LfpuPixJrE3uLB9u3q0rHxxTC8_AhX1QlYV--E

它由三部分组成,中间用点 . 隔开:

部分 名称 内容 作用
第一部分 头部(Header) {"alg":"HS256","typ":"JWT"} 的 Base64Url 编码 声明签名算法,告诉你这是身份证
第二部分 负载(Payload) 你存的信息(如 {"id":1,"username":"zhangsan"})的 Base64Url 编码 存放业务数据 ,告诉你身份证上的信息
第三部分 签名(Signature) 用密钥对前两部分计算出来的哈希值,再 Base64Url 编码 防篡改

注意 :三个部分全部都要进行 Base64Url 编码 ,包括签名!

签名本身是二进制哈希值,必须编码成可见字符才能放在 HTTP 头或 URL 里

我们可以去 https://jwt.io 网站上粘贴令牌,它会自动解码出头部和负载的内容。

二、要搞清楚几个核心概念

2.1 如图:为什么需要编码为什么需要解码?

2.1.1 先解释什么是编码(Encoding)和解码(Decoding)

编码 :把一段文本转换成另一串字符,方便传输。
解码:把编码后的字符还原成原文。

Java 自带的 Base64 编码/解码示例:

java 复制代码
import java.util.Base64;

public class CodecDemo {
    public static void main(String[] args) {
        // 原文
        String original = "{\"id\":1,\"name\":\"张三\"}";
        System.out.println("原文:" + original);

        // 编码(Base64Url 格式,适合放在 URL 里)
        String encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(original.getBytes());
        System.out.println("编码后:" + encoded);

        // 解码
        byte[] decodedBytes = Base64.getUrlDecoder().decode(encoded);
        String decoded = new String(decodedBytes);
        System.out.println("解码后:" + decoded);
    }
}

输出:

复制代码
原文:{"id":1,"name":"张三"}
编码后:eyJpZCI6MSwibmFtZSI6IuW8oOS4iSJ9
解码后:{"id":1,"name":"张三"}

结论

  • 编码和解码是互逆操作,不需要任何密钥,任何人都能做。
  • JWT 的头部、负载、签名都是用这种 Base64Url 编码的。

直接回答为何编码:

我们不是"故意"把信息编码成看不懂的样子,而是为了安全传输和防伪造 。你看到的那个像乱码一样的字符串,其实是 三部分拼在一起 的:头部(header)+ 负载(payload)+ 签名(signature) ,每一部分都是经过 Base64Url 编码的。


2.1.2、为什么不直接传 JSON 明文?

假设你直接传这样一段 JSON:

json 复制代码
{
  "userId": 123,
  "userName": "张三",
  "exp": 1698399660
}

问题1:传输不安全

HTTP 是明文传输的(除非你用 HTTPS),中间人很容易截获并修改内容。即使你用 HTTPS,传输过程中也可能因为特殊字符(比如换行、引号)导致解析出错。

问题2:无法防伪造

直接传 JSON,谁都可以自己造一个假的 JSON 塞给你。服务器无法判断这个 JSON 到底是你签发的,还是别人伪造的。

问题3:URL 不安全

如果放在 URL 参数里(比如 ?token={...}),JSON 里的 {}": 等字符需要大量转义,很麻烦。


2.1.3、JWT 是怎么解决这些问题的?

JWT 把三部分分别编码成 Base64Url 格式(只包含字母、数字、-_),拼在一起,中间用点 . 隔开。这样:

  • 所有特殊字符都没了,可以安全放在 URL、Header 里。
  • 最重要的:第三部分签名。签名是用密钥对前两部分(头部+负载)计算出来的。任何人篡改前两部分,签名就会失效,服务器一校验就知道被改过。

所以,JWT 不是加密,而是编码 + 签名

编码只是为了让数据能安全地在网络上传输,签名才是防伪造的核心。


2.1.4、那为什么我们拿到后还要解码?

因为你需要读出里面的信息啊!比如你存了 userIduserName,你总要知道是哪个用户在访问吧?

解码就是 Base64Url 的反过程,把那一串乱码还原成 JSON 字符串。服务器在解码的同时,还会验证签名,确保数据没有被篡改。

图片里说的"解码有效载荷"就是这个意思:

  • 左边那串 eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXWCj9 解码后得到 {"alg":"HS256","typ":"JWT"}
  • 中间那串解码后得到你存的用户信息。

2.1.5、一句话总结

不直接传 JSON,是因为要加上签名防止伪造,同时编码成 URL 安全的字符串方便传输。解码是为了读取里面的内容,并验证签名是否有效。

所以你看,JWT 并不是故意"加密"让你看不懂,而是为了安全和方便。你完全可以用官网提供的解码工具看到里面的内容,但一旦有人改了任何一个字符,签名校验就会失败,服务器会拒绝这次请求。这就是 JWT 的防篡改原理。


2.2 密钥(Secret Key)到底是什么?

密钥就是一个保密的字符串(或字节数组) ,只有服务器知道。

它的作用:用来给消息计算签名,防止消息被篡改。


注意:其实校验用的是密钥对象 ,不是密钥字符串。

不过在日常交流中,大家经常会简化为"用密钥校验",因为密钥对象就是密钥的载体。


你可能会问:"一个字符串怎么能用来计算签名?"

因为签名算法(如 HMAC-SHA256)可以把"密钥"和"消息内容"一起作为输入,计算出一个固定长度的哈希值(签名)。

只要密钥不同,即使消息相同,签名也完全不同。

没有密钥的 JWT 就是一张"白纸"

假设你用 Base64 编码用户信息:{"id":1,"username":"zhangsan"} 编码成 eyJpZCI6MSwidXNlcm5hbWUiOiJ6aGFuZ3NhbiJ9

你把这个字符串发给服务器,服务器解码后就知道你是张三。

但是,坏人截获了这个字符串,他也解码看到了结构,然后他把自己改成 {"id":2,"username":"lisi"},再编码成新的字符串,发给服务器。服务器解码后发现是李四,就以为他是李四。
因为没有任何东西能证明这个字符串是服务器当初发的,还是坏人伪造的

密钥的作用就是用来生成签名,防伪造。

服务器生成令牌时,用密钥(jwt使用的是密钥对象)对头部负载进行加密计算,得到一串"指纹"(签名),附在令牌后面。

因为密钥只有服务器知道,别人无法伪造签名。

当客户端带着令牌回来时,服务器用同样的密钥(JWT使用的是秘钥对象[对密钥封装之后的对象称为密钥对象])重新计算签名,和令牌自带的签名比对。

  • 如果一致 → 说明令牌没被篡改,是服务器发的。
  • 如果不一致 → 说明被改过,直接拒绝。

所以,密钥(密钥对象)不是 JWT 令牌里的一部分,而是服务器自己保管的一个秘密字符串。没有密钥,JWT 就失去了防篡改能力,跟普通编码没区别。


2.3 签名(Signature)

签名是用密钥(JWT使用的是密钥对象)对消息内容计算出的哈希值,用来防止消息被篡改。

在 JWT 中,签名的消息是:编码后的头部 + "." + 编码后的负载

JWT中签名的生成流程:

特点

  • JWT中同样的密钥对象 + 同样的消息 得到 同样的签名。
  • 消息被改动任意一个字符,签名就会完全不同。
  • 不知道密钥的人无法伪造正确的签名。

签名不是加密,它不隐藏消息内容,只保证消息没有被篡改。


2.4 编码 vs 签名(一句话总结)

  • 编码 :把 JSON 变成一段可见字符(Base64Url),任何人都能解码,只是为了方便传输。
  • 签名 :用密钥对消息内容计算哈希值,只有有密钥的人才能验证,用来防篡改。

JWT = 编码(方便传输) + 签名(防伪造)


三、密钥存在哪?前端能拿到吗?多服务器怎么用?

3.1 密钥存在哪里?

密钥只存在后端服务器,通常放在:

  • 配置文件里(如 application.yml
  • 环境变量里(更安全)
  • 专门的配置中心(如 Apollo、Nacos)

绝对不要硬编码在代码里,更不要提交到 Git 仓库。

3.2 前端能拿到密钥吗?

绝对不能!

密钥是服务器的"私章",一旦泄露,任何人都可以伪造令牌。前端只拿到最终生成的令牌字符串,永远接触不到密钥。

3.3 多个服务器(集群)怎么用?

如果你的系统部署在多台服务器上(比如 3 台机器做负载均衡),所有服务器必须使用完全相同的密钥

为什么?

  • 用户在 A 服务器登录,A 服务器用密钥 K 生成令牌。
  • 用户后续请求被负载均衡转发到 B 服务器,B 服务器也必须用同一个密钥 K 才能验证这个令牌。
  • 如果密钥不同,B 服务器验证签名就会失败,用户会被强制重新登录。

所以:所有后端服务器共享同一个密钥(通过配置中心或环境变量统一设置)。


四、生成令牌(genJwt)------ 分步详解

4.1 准备工作:密钥和过期时间

java 复制代码
// 过期时间(30分钟,单位毫秒)
public static final long EXPIRATION = 30 * 60 * 1000;

// 密钥字符串(Base64 编码的,长度要够,可以用工具生成)
// 注意:这个字符串只存在于后端配置中,前端拿不到
private static final String SECRET_STRING = "HtZrNdvOQYgg6QxLMyXYqVAOwPD6Ue+lHpXho5m54O0=";

// 把密钥字符串解码成字节数组,再生成 HMAC-SHA256 算法的密钥对象
private static final SecretKey KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_STRING));

为什么要把密钥字符串先解码成字节数组,再生成 SecretKey 对象?

  • 签名算法(HMAC-SHA256)要求密钥是字节数组(byte[]),不是字符串。
  • 我们为了方便配置,先借助api生成一个随机的密钥对象,再通过密钥对象的提取数里面的二进制密钥,再把二进制密钥转换为字符串密钥,这样方便我们配置。
  • 上面的字符串是 Base64 编码 的,Decoders.BASE64.decode(SECRET_STRING) 把它解码成原始字节数组。
  • Keys.hmacShaKeyFor() 是 JJWT 库提供的方法,它把字节数组包装成适合 HMAC 算法使用的 SecretKey 对象。

如何生成安全的密钥字符串? 可以用下面的代码生成一个,然后放到配置里:

java 复制代码
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String secret = Encoders.BASE64.encode(key.getEncoded());
System.out.println(secret);

也就是说:

🤔JWT当中第3段,签名靠什么生成?

这里老铁请记住,签名是通过密钥+前面两段,再通过一个哈希算法得到的。可是想到这儿,再次思考,我们上述代码中,你会观察到:第一段头部由我们的构建器,默认生成,第2段载荷是我们用户自己定义,这第3段签名是一个秘钥对象(Key对象)。

🤔老铁你看图中的第3段,他要的规格是密钥对象,也就是说我们不是把一串数字直接当做密钥去校验,而是呢,把它封装成一个对象密钥对象Key,最后再使用这个密钥对象,结合1、2段编码后进行一个哈希,得到第3段二进制签名,再把第3段进行编码,最后呢将123段拼接成字符串,得到我们的令牌token。

Ok,老铁,到这一步,你知道,我们进行校验用的是密钥对象,而不是一个单一的字符串。

接下来我们就抛出了这样的一个疑问:

🤔这个密钥对象key怎么生成?它有固定的接口,供我们调用吗?

没错,它就是有固定的接口,供我们去调用,直接生成这个密钥对象。

🤔那很简单的呀,为什么我们还要用上述那么麻烦的逻辑呢,我们直接用这个api去生成密钥对象,进行校验不就行了吗?

事实并非如此,老铁你看这样的方式它有一个缺陷,你这样写的话,你每次重启服务器,它都会重新生成一个新的密钥对象。每次都用这个新的密钥对象去进行签名的话,我们就每一次重启服务器,都有不同的签名。换句话说,你每次重启服务器,你都有一个令牌,到时候你把这个令牌是给我们的前端的呀,比如前端用户a拿到这个令牌1之后,你突然间重启服务器了,你后台又有一个新的令牌2,你得校验通过这个新的令牌2的话。而用户a拿着原来的令牌1去校验,发现校验不对,这就问题大了。很显然用不了这种方式。

🤔那怎么办呢?

其实道理很简单,就比如我们的集群服务器,不同的服务器,你最终共用一个密钥对象,也就是说你的这个密钥对象得是固定的,你这个密钥对象保持唯一性,或者说保持固定性不变,那么无论你怎么重启服务器,它都不变的话,你前端永远都是使用这个令牌去后端进行校验。还会出现上述的问题吗?绝对不会。

🤔那如何让这个密钥对象唯一啊?

老铁,这里咱们的技巧性就很能体现出来:你先看下图。

你看,我们用这个 API呢,可以得到一个安全的密钥对象,所以呢,我们先随机让它生成一个安全的密钥对象,而这个密钥对象中有什么?有二进制密钥数据。这个二进制没有数据我们读不懂呀,但是不用怕,我们可以把它进行编码,编码成我们看得懂的,字符串数数据就是密钥字符串,如下图所示:

此时你看我们可以从密钥对象中取出二进制密钥数据,然后把这个二进制密有数据给编码成一个可以看得懂的字符串密钥:

text 复制代码
HtZrNdvOQYgg6QxLMyXYqVAOwPD6Ue+lHpXho5m54O0=

🤔你好端端的生成这个字符串密钥干嘛呢?

首先你就说这个字符串密钥你能不能看得明白吧,反正字母能看得懂,对吧?那既然如此,我们可以拿它来进行配置呀,为什么呢?你看:你生成的密钥对象中有二进制密钥,你把二进制密钥可以编码成密钥字符串,那反过来,你可以将这个密钥字符串进行解码成二进制密钥呀。而你解码成二进制密之后,你可以用api对它进行封装,再封装成一个密钥对象,这不就闭合了吗?我们将得到的这个密钥字符串配置在某个地方,固定死在我们的后端,让它保持一个永远不会变的状态。然后我们每一次生成的密钥对象,都通过这个钥字符串先转为二进制密钥,再将这个二进制密钥封装成密钥对象。如此一来,只要你的这个密钥字符串不变动,那么我们的密钥对象他就不会改变,逻辑如下图所示:

好,到这里我们再回头看之前我们很难理解的那两行代码:

这两行代码的解释如下图所示:

总结:我们需要的是固定的一个密钥对象,不是随机的,为了达到这个目的,你就用人家的工具生成,先随机生成一个密钥对象,就是为了达到让这个密钥对象永久存在服务器,达到这样的一个要求,方便配置的话,我们得把它转为密钥字符串,为了得到密钥字符串,得需要从这个密钥对象中提取出它的二进制密钥,通过二进制密钥就可转为密钥字符串,然后把得到的这个密钥字符串配置在我们的服务器中永久存起来。固定存起来,我们所有的服务器都只用这一个。但是我们这Jwt校验的时候,生成数字签名用的密钥是密钥对象,而不是我们的密钥字符串,所以此时就得将这个密钥字符串转为密钥对象,这个密钥字符串转为密钥对象的前提是先把密钥字符串转为二进制的密钥,然后再通过二进制密钥传递进去,变成密钥对象。


4.1.1 简化方式:直接生成随机密钥(仅限测试)

你可能会问:"项目里搞得这么复杂,我用下面这一行代码不也能生成密钥吗?"

java 复制代码
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

这一行代码确实不会报错 ,而且用起来非常简单。它每次调用都会生成一个全新的、随机的、足够安全的密钥对象

但是,它有一个致命问题

  • 项目第一次启动,生成密钥 K1,签发了一些令牌给用户。
  • 项目重启,又会生成一个全新的密钥 K2(和 K1 不同)。
  • 用户之前拿到的令牌是用 K1 签名的,现在服务器用 K2 去校验,签名验证失败,所有用户被强制踢下线,必须重新登录。

这在生产环境是灾难!

所以,两种方式对比如下:

方式 代码 特点 适用场景
随机生成 Keys.secretKeyFor(HmacAlgorithms.HS256) 每次运行生成不同密钥 仅限测试、临时演示
固定密钥 Keys.hmacShaKeyFor(Decoders.BASE64.decode("固定字符串")) 永远生成同一个密钥 生产环境、正式项目

结论

  • 测试时可以用 Keys.secretKeyFor(...) 偷懒。
  • 正式项目必须用固定密钥(即博客里写的"复杂"方式),否则用户一重启就得重新登录,没人能接受。

4.2 生成令牌的完整代码(分步版,非链式调用)

java 复制代码
@Test
public void genJwt() {
    // 第1步:准备要存进令牌的数据(就像填身份证申请表)
    //用于后面的载荷payload
    Map<String, Object> claims = new HashMap<>();
    claims.put("id", 1);
    claims.put("username", "zhangsan");

    // 第2步:获取 JJWT 的建造器(相当于拿到一张空白身份证卡片)
    //默认生成了jwt中头部header
    JwtBuilder builder = Jwts.builder();

    // 第3步:把数据放进去(这一步只是暂存,还没有编码)
    builder.setClaims(claims);

    // 第4步:设置签发时间(相当于办证日期)
    builder.setIssuedAt(new Date());

    // 第5步:设置过期时间(30分钟后)
    builder.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION));

    // 第6步:设置签名算法和密钥(相当于公安局在卡片上盖章)
    builder.signWith(KEY);

    // 第7步:生成最终的令牌字符串
    // 这一步内部干了四件事:
    //   ① 把头部 JSON 转成 Base64Url 字符串(编码)
    //   ② 把负载 JSON 转成 Base64Url 字符串(编码)
    //   ③ 用密钥对 ①(编码后的) + "." + ②(编码后的) 计算签名(二进制哈希值),再把签名二进制转成 Base64Url 字符串(编码)
    //   ④ 把①②③三部分用点拼接成最终字符串
    String jwt = builder.compact();

    System.out.println(jwt);
}

注意:签名部分也要编码!因为签名是二进制数据,必须编码成可见字符才能放在令牌里。


4.3 生成令牌逻辑

好的老铁们,咱们一步一步的来完成生成令牌的逻辑,首先呢,咱得知道令牌就是我们的token,而我们的jwt它是一个标准的令牌格式。这个令牌(Token)的格式如下,由三部分组成:

那么我们想构建一个这样的令牌,这样的token,无非就是干4件事:

  • 生成头部部分JSON,再用Base64编码为字符串。
  • 生成载荷部分JSON,再用Base64编码为字符串。
  • 生成签名,再用Base64编码为字符串。
  • 最后一件事,用点号.将上述的三个拼接起来。

1️⃣ 咱先完成生成头部数据:这个头部信息呢,它会自动给我们生成,也就是说我们调用构建器方法的时候,它就自动生成的,如下图所示。

2️⃣接下来咱完成头部数据之后,我们就完成我们的载荷部分的数据。载荷部分是我们用户自定义的,它主要包括我们的用户信息,签发的时间,以及过期时间,那得到的步骤呢,大概如下所示的:
3️⃣ 头部和载荷我们都完成了,就只有签名了,那接下来我们就完成签名,签名的流程如下所示:

第1步,根据我们的那个持久的密钥字符串转为二进制密钥,再将二进制密钥作为参数传进去,生成我们的密钥对象,如下:

得到密钥对象之后,我们进行下一步。

第2步:我们生成密钥对象之后,将这个密钥对象传递到我们的构建器中,提前做好构建令牌的准备,如图:

第3步:把头部JSON数据通过Base64编码为字符串得到编码1,再把载荷JSON数据通过Base64编码字符串编码2,如图:

第4步,使用密钥对象编码1+"."+编码2 进行计算,得到一个二进制哈希值,这个二进制哈希值就是我们的签名,即二进制签名,再把二进制签名通过Base64编码为字符串编码3,如图:

4️⃣ Ok,那到目前为止,我们上周已经完成了头部以及头部的编码,载荷以及载荷的编码,签名以及签名的编码。只有最后一步就是通过点将三个编码1编码2编码3给拼接起来,就得到了我们的令牌token如图:


4.4 编码发生在哪一步?

发生在第7步 builder.compact() 内部
compact() 会自动完成头部编码、负载编码、签名计算、签名编码、三部分拼接。

4.5 函数解释

setClaims()解释


compact()解释

signWith()解释


secretKeyFor()解释

SignatureAlgorithm.HS256解释

setIssuedAt()解释

setExpiration()解释


五、校验令牌(parseJWT)------ 分步详解

java 复制代码
@Test
public void parseJWT() {
    // 假设这是客户端带上来的令牌
    String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsImlhdCI6MTY5ODM5Nzg2MCwiZXhwIjoxNjk4Mzk5NjYwfQ.vVPraAT_i_--pUSi2s_gwtdQmNFVEao6xmk6eLqpQSw";

    // 第1步:创建解析器的建造器
    JwtParserBuilder parserBuilder = Jwts.parserBuilder();

    // 第2步:设置验证签名用的密钥(必须和生成时用的密钥一模一样)
    parserBuilder.setSigningKey(KEY);

    // 第3步:建造出真正的解析器
    JwtParser parser = parserBuilder.build();

    // 第4步:解析令牌(这一步内部干了四件事)
    //   ① 把令牌按点拆成三部分字符串
    //   ② 对第一部分和第二部分进行 Base64Url 解码,得到头部 JSON 和负载 JSON
    //   ③ 对第三部分进行 Base64Url 解码,得到签名的二进制数据
    //   ④ 用密钥重新计算签名,与解码后的签名比对;同时检查过期时间
    // 如果签名不对或过期,这里会直接抛异常
    Jws<Claims> jws = parser.parseClaimsJws(token);

    // 第5步:从解析结果中取出负载(Claims)
    Claims claims = jws.getBody();

    // 第6步:从负载里取出之前存的数据
    System.out.println(claims.get("id"));
    System.out.println(claims.get("username"));
}

5.1 校验令牌逻辑

Ok,咱们像上述生成令牌的方式一样,一步一步的来讲解如何校验令牌。

1️⃣我们的前端去请求的时候,是会在请求头中带上我们服务器给他签发的token的,假设我们的token如下图所示:

那么接下来你就想我们要去校验,校验的话,如何校验呢?大概的思路是这样子的。我们有一个空的盒子,这个空的盒子,我们把它叫做构建器,就是用来构建校验的那个机器的,校验的机器有哪些组件呢?第1个组件就是我们的密钥对象,这个组件就是我们的验钞机,也就是说,在验钞机上配上一把钥匙,这把钥匙去开机我们的验钞机,我们的验钞机呢才能正常工作,才能对我们的钞票进行校验,所以有了这个大概思路之后,我们就继续往下走。

2️⃣我们得先有一个空盒子,就是用来构建验证钞票那个机子的一个制造厂,我们把它称之为构建器,如图:

3️⃣有了构建器之后,那我们就可以通过这个构建器来给我们制造出一个验钞机了,但是制造验印钞机一个很重要的前提条件就是:这个印钞机,你得有一个钥匙去把这个机子给启动,所以我们先去配一把钥匙。这个钥匙就是我们的密钥对象,如图所示:

4️⃣ 我们放入钥匙之后,此时我们就可以去找另外一个零件,就是我们的验钞机,然后我们就可以使用这把钥匙把我们的验钞机给打开,我们就可以进行验钞了【只是准备好,但没有开始】,如图:

5️⃣接下来我们就开始去验钞了,也就是说开始去解析令牌了,那它的步骤如下图所示:

第1步:理解 parser.parseClaimsJws(token);的意思,如图:
第2步:把令牌按照点拆成三个字符串,如图所示:
第3步:对头部和载荷部分使用Base64进行解码,变成头部JSON和载荷JSON,为什么需要解码为json对象,因为我们要获取里面的值呀,我们到时候是要获取里面的一些信息的,比如说用户的id如图:

第4步:对签名部分使用Base64解码得到二进制签名,也就是一个二进制哈希值,为什么将它解码成一个二进制哈希值1,目的就是为了我们的第5步,重新计算一个二进制哈希值2与该二进制哈希值1进行对比。如下图:

第5步:使用密钥对象对编码1+"."+编码2重新计算得到二进制签名2,然后和我们第4步中得出的二进制千名一进行比对,如下图【注意一点,我们重新计算二进制签名的时候,用的仍然也是编码之后的头部和载荷,而不用原始的JSON数据】:

第6步:如果比对二进制签名2和二进制签名1他们是一致的话,那么我们再检查一下过期时间那些等等。如果我们两个签名不一致,或者说时间过期了的话,那此处就会直接抛一个异常,程序不会再往下走了,那此时你就可以。那不用返回任何载荷对象了,你返回的是一个空,我们就可以判断出令牌失效,重新登录。

如果一切正常的话,我们就会返回我们的载荷对象,那我们收到对象就代表当前这个token是有效的,允许你访问。

6️⃣一切正常的话,我们就继续做,如下事情:

Ok,到这里,我们令牌的校验逻辑就结束了~~


5.2 解码发生在哪一步?

发生在第4步 parser.parseClaimsJws(token) 内部

它会自动完成拆包、解码头部、解码负载、解码签名、验签、检查过期。


5.3 函数解释

parserBuilder()解释

setSigningKey(key)解释


六、一张表看清编码/解码、生成/校验的关系

操作 具体内容 需要密钥吗 在哪一步发生
编码 JSON → Base64Url 字符串 compact() 内部
解码 Base64Url → JSON parseClaimsJws() 内部
签名 用密钥对编码后的头部+负载计算哈希值 compact() 内部
验签 用密钥重新计算签名并比对 parseClaimsJws() 内部
生成令牌 编码头部 + 编码负载 + 签名并编码 + 拼接 调用 generateToken
校验令牌 拆包 + 解码头部/负载/签名 + 验签 调用 parseToken

一句话总结

  • 编码/解码只改变格式,不需要密钥。
  • 签名/验签是防篡改的核心,必须依赖密钥
  • 生成令牌 = 打包 + 签名 + 编码(三部分都要编码)。
  • 校验令牌 = 拆包 + 解码 + 验签。

七、在项目里实际怎么用?(完整工具类)

java 复制代码
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.io.Decoders;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.Map;

@Slf4j
public class JwtUtils {
    // 过期时间:24小时(单位毫秒)
    private static final long EXPIRATION = 24 * 60 * 60 * 1000;
    
    // 密钥字符串(实际使用时从配置文件读取)
    private static final String SECRET_STRING = "dVnsmy+SIX6pNptQdeclDSJ26EMSPEIhvZYKBTTug4k=";
    
    // 生成密钥对象
    private static final SecretKey KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_STRING));

    /**
     * 生成 JWT 令牌
     * @param claims 要存储的用户信息(比如 id、username)
     * @return JWT 字符串
     */
    public static String generateToken(Map<String, Object> claims) {
        JwtBuilder builder = Jwts.builder();
        builder.setClaims(claims);
        builder.setIssuedAt(new Date());
        builder.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION));
        builder.signWith(KEY);
        return builder.compact();
    }

    /**
     * 校验并解析 JWT 令牌
     * @param token 客户端传来的令牌字符串
     * @return 解析后的 Claims,如果无效则返回 null
     */
    public static Claims parseToken(String token) {
        try {
            JwtParserBuilder parserBuilder = Jwts.parserBuilder();
            parserBuilder.setSigningKey(KEY);
            JwtParser parser = parserBuilder.build();
            Jws<Claims> jws = parser.parseClaimsJws(token);
            return jws.getBody();
        } catch (Exception e) {
            log.error("令牌无效或已过期", e);
            return null;
        }
    }
}

7.1 在登录接口中生成令牌

java 复制代码
// 验证账号密码通过后
Map<String, Object> claims = new HashMap<>();
claims.put("id", userInfo.getId());
claims.put("username", userInfo.getUserName());
String token = JwtUtils.generateToken(claims);
return new UserLoginResponse(userInfo.getId(), token);

7.2 在拦截器中校验令牌

java 复制代码
String token = request.getHeader("user_token");
if (token == null) {
    response.setStatus(401);
    return false;
}
Claims claims = JwtUtils.parseToken(token);
if (claims == null) {
    response.setStatus(401);
    return false;
}
// 令牌有效,可以从 claims 中取出用户信息(比如用户id)
request.setAttribute("userId", claims.get("id"));
return true;

八、总结

  1. 密钥是服务器自己的秘密,用来给令牌签名,防止别人伪造。前端永远拿不到密钥。
  2. 所有后端服务器(集群)必须使用完全相同的密钥,否则令牌无法跨机器验证。
  3. 编码/解码 :只改变数据格式,任何人都能做,不保证数据没被篡改
  4. 生成令牌 = 打包信息 + 签名 + 编码(三部分都要编码)。
  5. 校验令牌 = 拆包 + 解码 + 验签。
  6. 没有密钥的 JWT,就跟明文身份证一样,谁都能伪造。

老铁们觉得有用,别忘了点赞、收藏、关注,咱们下期见咯~~🚀

相关推荐
庞轩px3 天前
JWT与Session比较
jwt·session·登录鉴权·无状态
庞轩px4 天前
JWT + Redis 双 Token 机制:从原理到实战
数据库·redis·缓存·jwt·token·登录认证
带娃的IT创业者5 天前
WeClaw_43_双重认证与Token自动刷新:Device Fingerprint与JWT安全机制
jwt·认证机制·双重认证·设备指纹·token刷新·http安全
禹笑笑-AI食用指南11 天前
# 从“发 Key”到“管 Key”:一个本地大模型 Key 管理中心的难点与落地方案
key·密钥·大模型密钥·大模型成本管理
૮・ﻌ・19 天前
Node.js - 04:MongoDB、会话控制
数据库·mongodb·node.js·jwt·token·cookie·session
indexsunny19 天前
互联网大厂Java面试实战:从Spring Boot到微服务架构的音视频场景解析
java·spring boot·spring cloud·mybatis·spring security·jwt·flyway
没有bug.的程序员20 天前
撕裂微服务网关的认证风暴:Spring Security 6.1 与 JWT 物理级免登架构大重构
java·spring·微服务·架构·security·jwt
独断万古他化21 天前
【抽奖系统开发实战】Spring Boot 项目的用户模块设计:注册登录、权限管控与敏感数据加密
java·spring boot·redis·后端·mvc·jwt·拦截器
沉默-_-1 个月前
JWT详解:从登录认证到令牌验证
jwt·javaee