JWT

JWT (JSON Web Token) 的结构。一个 JWT 由三部分组成,通过点 (.) 连接:

xxxxx.yyyyy.zzzzz

  1. Header (头部) xxxxx

  2. Payload (载荷) yyyyy

  3. Signature (签名) zzzzz


1. 解码 (Decoding) - 不需要密钥

JWT 的 HeaderPayload 这两部分是使用 Base64Url 编码的 JSON 对象。Base64Url 是一种编码方式,不是加密。它的作用是将二进制数据转换成文本字符,以便在网络中安全地传输。

解码 指的就是将这两部分的 Base64Url 编码字符串还原成原始的 JSON 文本。这个过程是公开的、可逆的,完全不需要任何密钥

任何人只要拿到一个 JWT,都可以轻易地解码前两部分,读取里面的信息。

  • Header (头部): 通常包含令牌的类型(typ,即 “JWT”)和所使用的签名算法(alg,例如 HS256RS256)。

  • Payload (载荷): 包含需要传递的数据,也称为“声明 (Claims)”。例如用户ID、用户名、过期时间 (exp) 等。

Note

如果你仅仅是想“看一看”JWT里面存了什么信息(Header 和 Payload),你不需要密钥,只需要进行 Base64Url 解码即可。这也是为什么 敏感信息(如密码)绝对不能直接放在 JWT 的 Payload 中


2. 验证 (Verification) - 必须需要密钥

JWT 的第三部分是 Signature (签名)。它的作用是验证这个 JWT 是否合法、是否在传输过程中被篡改过

签名的生成过程如下:

HMACSHA256(base64UrlEncode(header) + ”.” + base64UrlEncode(payload), secret)

这个过程使用了在 Header 中声明的签名算法(例如 HS256),并结合一个密钥 (secret/key) 来对前两部分编码后的字符串进行加密计算。

验证签名 的过程,就是接收方用相同的算法和相同的密钥,对接收到的 Header 和 Payload 进行一次同样的签名计算,然后将计算出的新签名与 JWT 中自带的第三部分(zzzzz)进行比对。

  • 如果两个签名一致,说明:

    1. 这个 JWT 确实是由持有密钥的合法发送方签发的。

    2. JWT 的 Header 和 Payload 在传输过程中没有被任何人修改过。

  • 如果两个签名不一致,则说明这个 JWT 是伪造的或被篡改了,应该立即丢弃。

Warning

验证 JWT 的真实性和完整性必须使用密钥。没有密钥,你就无法确认这个 JWT 是否可信。


结论

在实际应用中,服务器收到一个 JWT 后,几乎总是会先验证签名。只有在验证通过后,才会信任并使用从 Payload 中解码出的信息。因此,虽然单纯的“解码”不需要密钥,但在安全的业务场景下,“验证”是必不可少的一步,所以密钥至关重要。

JWT认证基本流程

sequenceDiagram
    participant Frontend as 前端
    participant Backend as 后端

    Note over Frontend,Backend: 用户认证流程
    Frontend->>Backend: 提交用户名和密码(登录请求)
    Backend->>Backend: 验证用户信息
    alt 认证成功
        Backend->>Backend: 生成JWT Token
        Backend-->>Frontend: 返回JWT Token
        Frontend->>Frontend: 存储JWT Token到本地
    else 认证失败
        Backend-->>Frontend: 返回认证失败信息
        Frontend->>Frontend: 显示登录错误提示
    end

    Note over Frontend,Backend: 后续接口请求流程
    Frontend->>Frontend: 从本地获取JWT Token
    Frontend->>Backend: 发送请求(携带JWT Token在请求头)
    Backend->>Backend: 拦截请求并验证JWT Token
    alt Token验证通过
        Backend->>Backend: 执行业务逻辑
        Backend-->>Frontend: 返回请求数据
        Frontend->>Frontend: 渲染页面展示数据
    else Token验证失败
        Backend-->>Frontend: 返回权限错误信息
        Frontend->>Frontend: 显示错误提示并跳转登录页
    end
  1. 登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
		jwtProperties.getAdminSecretKey(),
		jwtProperties.getAdminTtl(),
		claims);
  1. 后续请求中,前端会携带JWT令牌,通过JWT令牌可以解析出当前登录员工id:

参考sky-server/src/main/java/com/sky/interceptor/JwtTokenAdminInterceptor.java

//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
 
//2、校验令牌
try {
	log.info("jwt校验:{}", token);
	Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
	Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
	log.info("当前员工id:", empId);
	// 将员工id保存到当前线程中  
	BaseContext.setCurrentId(empId);
	//3、通过,放行
	return true;
} catch (Exception ex) {
	//4、不通过,响应401状态码
	response.setStatus(401);
	return false;
}

Tip

每次请求,前端会携带Jwt token,会触发JwtTokenAdminInterceptor.java,此时将解析到的用户Id使用ThreadLocal存储到当前线程中,方便后续调用