一、JWT令牌的定义

JWT(JSON Web Token)是一种基于JSON的开放标准(RFC 7519),用于在网络应用间安全传递声明信息。其核心特点是通过数字签名或加密实现数据的自包含性防篡改,无需依赖中心化存储即可完成身份验证。

核心特性

  1. 自包含性:令牌内直接携带用户身份、权限等关键信息,服务端无需查询数据库即可完成验证。

  2. 无状态性:适用于分布式系统,避免传统Session机制中的会话同步问题

  3. 跨域支持:通过HTTP头部传递,天然适配微服务架构和跨域资源共享(CORS)


二、JWT令牌的组成结构

JWT由三部分通过点(.)连接组成,格式为:Header.Payload.Signature

1. 头部(Header)

  • 作用:声明令牌类型与签名算法。
  • 编码方式:Base64URL编码

2. 载荷(Payload)

  • 作用:存储用户身份、权限及其他自定义数据。

  • 标准字段

    (RFC定义):iss(签发者)、sub主题)、exp(过期时间)、iat(签发时间)

  • 自定义字段:可添加业务相关数据(如userId、role),但需避免存储敏感信息(因Base64可逆解码)

3. 签名(Signature)

  • 作用:验证令牌完整性,防止篡改。
  • 生成方式:将Header和Payload的Base64编码字符串拼接,使用Header中指定的算法(如HS256)与密钥加密
  • 验证流程:服务端通过相同算法和密钥重新计算签名,与令牌中的签名比对

三、JWT令牌的适用场景

1. 单点登录(SSO)与跨域认证

  • 场景描述:用户在一个系统登录后,JWT可作为凭证在多个关联系统中复用,无需重复认证。例如,认证中心颁发JWT后,用户携带该令牌访问其他业务系统时可直接通过签名验证身份。
  • 优势:避免维护复杂的Session同步机制,减少服务器存储压力,尤其适合多域名、多子系统的分布式架构。

2. 分布式系统与微服务架构

  • 场景描述:在微服务间传递身份信息时,JWT无需依赖中心化Session存储,各服务可独立验证令牌有效性。例如,Kubernetes服务网格通过JWT实现服务间认证。
  • 优势:无状态特性简化服务扩展,降低系统耦合度,符合云原生设计原则。

3. 移动端与API认证

  • 场景描述:移动应用通过JWT实现API访问授权。客户端将令牌存储在本地(如iOS的UserDefaults或Android的SharedPreferences),每次请求携带令牌头。
  • 优势:避免依赖Cookie,适配移动端网络环境,且支持跨平台(如React Native、Flutter)。

4. 无密码认证与临时授权

  • 场景描述:用户通过“魔法链接”登录时,系统发送含JWT的链接至用户邮箱,点击后直接完成认证。
  • 优势:提升用户体验,减少密码管理风险,适用于短期操作(如密码重置)。

5. 第三方授权(OAuth 2.0)

  • 场景描述:在OAuth 2.0流程中,JWT作为访问令牌(Access Token)传递,资源服务可直接解析令牌中的权限声明,无需频繁查询授权服务器。
  • 优势:降低授权服务器负载,支持细粒度权限控制(如角色、作用域)。

四、JWT令牌的不适用场景

1. 需要频繁撤销权限或令牌的场景

  • 问题核心:JWT一旦签发即不可修改,若需立即撤销用户权限(如用户被封禁),只能等待令牌过期或强制刷新密钥,存在安全滞后性。
  • 替代方案:使用黑名单机制(将失效令牌存入Redis)或改用Session机制实时控制权限。

2. 长时间有效的令牌需求

  • 风险点:长期有效的JWT一旦泄露,攻击者可在过期前持续滥用。例如,用户离职后其令牌仍可访问敏感资源。
  • 解决方案:缩短令牌有效期(如1小时),配合刷新令牌(Refresh Token)延长会话。

3. 高并发与带宽敏感场景

  • 性能瓶颈:JWT体积较大(通常500B-2KB),在大量并发请求中会增加网络传输开销,影响响应速度。
  • 替代方案:对带宽敏感场景(如物联网设备)改用轻量级Token(如随机UUID)。

4. 需要存储复杂用户状态的场景

  • 局限性:JWT的无状态特性难以支持动态会话管理,例如需要跟踪用户在线状态、实时操作日志等。
  • 适用技术:传统Session机制或数据库存储会话详情。

5. 对安全性要求极高的敏感场景

  • 风险暴露:JWT默认仅签名不加密,载荷(Payload)可被Base64解码,若包含敏感信息(如用户ID、角色)易遭中间人窃取。
  • 改进措施:结合JWE(JSON Web Encryption)加密载荷,或仅存储非敏感标识符。

五、JWT令牌的快速入门使用

1.maven引入jwt

1
2
3
4
5
6
<!--  jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

2.创建jwt工具类,在里面创建两个方法

1
2
3
4
5
6
7
8
9
//创建jwt令牌
public static String generateJwt(Map<String, Object> claims){
...
}

//解析jwt令牌
public static Claims parseJWT(String jwt){
...
}

3.创建jwt令牌

  1. addClaims()添加自定义内容到有效荷载(payload)中。
  2. signWith():指定算法和密钥生成签名。
  3. setExpiration():设置过期时间(System.currentTimeMillis()是返回当前的时间)
  4. compact():生成最终的JWT字符串
1
2
3
4
5
6
7
8
9
10
private static String signKey = "luminous";//秘钥
private static Long expire = 43200000L;
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}

4.创建jwt解析

4.1. 创建解析器实例 (Jwts.parser())
  • 作用:初始化JWT解析器对象,准备进行令牌解析。
  • 底层实现:返回DefaultJwtParser对象,该对象包含解码、验签等核心逻辑。内部初始化压缩解码器(如DefaultCompressionCodecResolver)和时钟校验工具(DefaultClock)。
4.2. 设置签名密钥 (setSigningKey(signKey))
  • 关键作用:提供验证签名所需的密钥,确保令牌未被篡改。
  • 技术要点:密钥必须与生成JWT时使用的密钥完全一致,否则验签失败。。
4.3. 解析并验证令牌 (parseClaimsJws(jwt))
  • 执行流程:

    1. 拆分JWT:以.分隔符切分Header、Payload、Signature三部分,校验格式有效性。
    2. 解码头部:Base64URL解码Header,获取签名算法(HS256)。
    3. 验证签名:使用密钥重新计算签名,与JWT中的Signature比对,防止数据篡改。
  • 异常类型:

    • SignatureException:签名不匹配或密钥错误。

    • ExpiredJwtException:令牌已过期。

    • MalformedJwtException:JWT格式错误(如缺少部分)。

4.4. 提取声明数据 (getBody())
  • 返回值:Claims对象,本质是包含Payload数据的Map结构。
1
2
3
4
5
6
7
8
//解析jwt令牌
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
5.使用jwt

只需调用对应的这两个方法就可以,创建一个测试类,先生成jwt令牌

1
2
3
4
5
6
7
8
9
10
11
12
    @Test
public void test01() {
// 添加测试数据
Map<String, Object> m1 = new HashMap<>();
m1.put("id", 1);
m1.put("username", "admin");
m1.put("password", "admin");
// 生成JWT令牌
String jwt = JwtUtils.generateJwt(m1);
// 打印jwt令牌
System.out.println("生成的jwt令牌:"+jwt);
}

运行结果:

由于jwt令牌太长,图片无法完全展示,我将他复制下来
eyJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6ImFkbWluIiwiaWQiOjEsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3NDgwMTE0NDF9.znQ42CQWrd_lYoSmrNF2d1x20k8RH0tMdA-Di3kmQKU

接下来我们将生成jwt的方法注释掉,运行解析jwt方法

1
2
3
4
5
6
7
@Test
// 解析JWT令牌
public void test02() {
String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6ImFkbWluIiwiaWQiOjEsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE3NDgwMTE0NDF9.znQ42CQWrd_lYoSmrNF2d1x20k8RH0tMdA-Di3kmQKU";
Claims claims = JwtUtils.parseJWT(jwt);
System.out.println("解析后的jwt令牌:"+claims);
}

可以看到解析出来的内容跟我们之前设置的内容是一样的。这样,就完成了jwt令牌的生成和解析。