JWT
JWT (JSON Web Token) 的结构。一个 JWT 由三部分组成,通过点 (.) 连接:
xxxxx.yyyyy.zzzzz
-
Header (头部)
xxxxx -
Payload (载荷)
yyyyy -
Signature (签名)
zzzzz
1. 解码 (Decoding) - 不需要密钥
JWT 的 Header 和 Payload 这两部分是使用 Base64Url 编码的 JSON 对象。Base64Url 是一种编码方式,不是加密。它的作用是将二进制数据转换成文本字符,以便在网络中安全地传输。
解码 指的就是将这两部分的 Base64Url 编码字符串还原成原始的 JSON 文本。这个过程是公开的、可逆的,完全不需要任何密钥。
任何人只要拿到一个 JWT,都可以轻易地解码前两部分,读取里面的信息。
-
Header (头部): 通常包含令牌的类型(
typ,即 “JWT”)和所使用的签名算法(alg,例如HS256或RS256)。 -
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)进行比对。
-
如果两个签名一致,说明:
-
这个 JWT 确实是由持有密钥的合法发送方签发的。
-
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
- 登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);- 后续请求中,前端会携带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存储到当前线程中,方便后续调用