深入理解 JWT 中 Claims 的设计及其合理性

207 阅读4分钟

在使用 JWT(JSON Web Token)时,我们常常需要在 token 中存储一些用户或业务相关的信息,这些信息被称为 claims。从源码的角度来看,JJWT 库设计了一系列方法来设置 claims,它要求传入一个实现了 Map 接口的数据结构。这篇博客将带你从源码出发,详细讲解 JWT Claims 的设计思想、各种实现方式以及它们各自的合理性,并附上实际的代码示例。

image.png

JWT Claims 的设计要求

在 JJWT 库中,构建 JWT 的核心接口是 JwtBuilder。在这个接口中,我们看到与 claims 相关的方法有两个重载版本:

JwtBuilder setClaims(Claims var1);
JwtBuilder setClaims(Map<String, Object> var1);

这意味着你可以传入一个 Claims 对象,也可以传入一个普通的 Map<String, Object>。实际上,Claims 接口本身就扩展了 Map<String, Object>,因此从设计上来说,JWT 库期望所有的 claims 最终都是以键值对的形式存在。这种设计有以下几个优点:

  1. 灵活性高
    不论你使用哪个实现,只要它实现了 Map 接口,JWT 库都能通过遍历来序列化所有的 claims。你可以选择 Java 自带的 HashMap,也可以选择库中提供的 DefaultClaims
  2. 高效性能
    使用 HashMap 或类似的 Map 实现,其插入和查找操作都非常迅速,这在生成和解析 JWT 时能够提供足够的性能保证。
  3. 语义清晰
    通过使用 Claims 接口,代码语义上更加明确 —— 我们传递的是“声明”而不是普通的 Map,从而增强了代码的可读性。

不同方式设置 Claims 的实现

方式一:直接使用 HashMap

直接创建一个 HashMap 来存放所有你想要添加的声明,再一次性传递给 setClaims 方法。这种方式简单直观,适用于声明较少或简单的场景。

代码示例:

HashMap<String, Object> claims = new HashMap<>();
claims.put("id", "23");
claims.put("username", "大郎");

String token = Jwts.builder()
    .setClaims(claims)
    .signWith(SignatureAlgorithm.HS256, "wolfcode")
    .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000 * 24))
    .compact();

System.out.println(token);

方式二:使用 DefaultClaims

JJWT 库中提供了 DefaultClaims 类,它实现了 Claims 接口。使用 DefaultClaims 可以让代码语义上更贴近“声明”的概念,同时依然享受 Map 的高效性能。

代码示例:

DefaultClaims claims = new DefaultClaims();
claims.put("id", "23");
claims.put("username", "大郎");

String token = Jwts.builder()
    .setClaims(claims)
    .signWith(SignatureAlgorithm.HS256, "wolfcode")
    .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000 * 24))
    .compact();

System.out.println(token);

方式三:逐个添加声明

对于简单的场景,如果你只需要添加几个声明,也可以直接使用 claim(key, value) 方法逐个设置。这种方式省去了创建 Map 的步骤,代码更加简洁。

代码示例:

String token = Jwts.builder()
    .claim("id", "23")
    .claim("username", "大郎")
    .signWith(SignatureAlgorithm.HS256, "wolfcode")
    .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000 * 24))
    .compact();

System.out.println(token);

这种设计的合理性

从源码和设计角度来看,这样的设计是非常合理的,原因有以下几点:

  1. 符合 Map 接口标准
    由于 JWT 的 claims 实际上就是一组键值对数据,使用 Map 来存储它们符合直观的编程习惯,也与 JSON 的数据格式天然契合。
  2. 易于扩展
    开发者可以根据需求选择不同的实现方式(例如使用 HashMapDefaultClaims),也可以灵活地通过 claim(key, value) 方法添加单个属性。这种扩展性满足了各种复杂业务场景的需求。
  3. 解耦合
    JWT 库只依赖于 Map 接口,而不关心具体的实现细节。这样无论未来 Map 的实现有何种变化,或者你需要替换成其他自定义实现,代码都可以很容易适配。
  4. 性能考虑
    选择使用 HashMap 或 DefaultClaims 这样的高效数据结构,可以确保 JWT 构建和解析的过程中不会因为数据结构性能问题而成为瓶颈。

总结

JJWT 库通过要求传入实现了 Map 接口的数据结构来设置 JWT 中的 claims,实现了灵活、高效且语义明确的设计。开发者既可以选择直接传入 HashMap,也可以使用库中提供的 DefaultClaims,或者直接通过单个声明添加方法设置 claims。这些设计不仅符合 JSON 数据格式的自然属性,也让代码更加易于维护和扩展。