JWT 令牌的工作原理,结构和签名验证

2026-03-05开发工具
分享到

刚接触 JWT 那会儿还整出了bug,当时做一个移动端 API,为了 "方便",把用户手机号和角色信息直接塞进了 payload。同事 code review 时问了一句:"你确定这些数据安全吗?","JWT 嘛,加密的。"

同事后来直接用JWT 解析把我的 token 贴了进去。三秒钟,payload 里的手机号、角色、部门信息全部明文展示,真是当场石化~

那是才注意 JWT 的 payload 只是 Base64 编码,不是加密。Base64 编码的本质是把二进制数据转成文本,方便传输,它不需要密钥就能解码。

JWT 看起来简单,三个点号分隔的字符串而已。但踩过坑就知道,背后涉及的东西远比表面复杂。

JWT 的结构

JWT 的全称是 JSON Web Token,规范定义在 RFC 7519。它的格式固定,三段式结构:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiSm9obiIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJVadQssw5c

三段用点号隔开,分别是 Header、Payload、Signature。每一段都是 Base64URL 编码的。Base64URL 是标准 Base64 的变体,把 + 换成 -/ 换成 _,去掉末尾的 =。为什么要这么改?因为标准 Base64 里的 +/ 在 URL 里有特殊含义,+ 被解释成空格,/ 是路径分隔符。JWT 经常出现在 URL 参数或 HTTP 请求头里,必须用 URL-safe 的编码。

合起来就是:JWT = Base64URL(Header) + '.' + Base64URL(Payload) + '.' + Signature

注意,Header 和 Payload 只编码,不加密。任何人都可以解码看到原始内容。Signature 才是保证数据完整性的关键。

JWT 结构:三部分解析

Header 里面有什么

Header 是一个 JSON 对象,通常包含两个字段:

{
  "alg": "HS256",
  "typ": "JWT"
}

alg 是签名算法。常见的值有 HS256(HMAC + SHA-256)、RS256(RSA + SHA-256)、ES256(ECDSA + SHA-256),甚至还有 none(不做签名)。这个字段是整个 JWT 安全性的关键入口——如果服务端不校验 alg 的值,攻击者可以把它改成 none 绕过签名验证。后面聊漏洞的时候会细说。

关于 alg 还有个容易忽略的点:同一个算法名在不同 JWT 库里的实现可能有差异。比如 PS256 和 RS256 都是 RSA 家族,但 PS256 用了 PSS 填充模式,RS256 用的是 PKCS1.5 填充。有些库把两者混为一谈,有些严格区分。跨语言调用 JWT 时(比如 Node.js 签发,Go 验证),算法兼容性问题经常冒出来。

typ 是令牌类型,固定为 JWT。有些实现会忽略这个字段,但规范里建议加上。如果你看到 JWE 或者 JWT 以外的值,说明这个 token 可能不是标准的 JWT。

还有一个可选字段 kid(Key ID)。当服务端有多组签名密钥时(比如轮换密钥的场景),用 kid 告诉验证方该用哪个密钥来验签。这个字段在多租户系统和微服务架构里很常见。kid 的值通常是个指纹或者 UUID,和 JWKS 里的 kid 一一对应。

有时还会看到 cty(Content Type),用来描述 payload 的内容类型。大多数场景用不到。

Header 的设计看似简单,但它直接影响签名的安全性。一个常见的误配置是把 alg 写死在代码里但不校验——服务端直接从 token 的 header 里读 alg 来决定用哪个算法验证。你猜会发生什么?攻击者把 alg 改成 none,你的签名验证就跳过了。

Payload 里的声明

Payload 也是一个 JSON 对象,里面的字段叫作 "声明"(Claims)。声明分三种:注册声明、公共声明、私有声明。

注册声明是规范里预定义的,不强制但建议用:

  • sub(Subject):令牌的主体,通常是用户 ID。用来标识这个 token 属于谁。sub 在整个签发系统内应该是唯一的。
  • iat(Issued At):签发时间,Unix 时间戳格式。服务端可以拒绝签发时间在未来的 token,也可以拒绝太旧的 token(比如签发时间超过 24 小时的 token 直接不处理)。
  • exp(Expiration Time):过期时间。超过这个时间 token 就失效了。这个字段必须检查——不检查 exp 等于让 token 永久有效。每个请求都应该验证 exp,JWT 库默认不会自动检查。
  • nbf(Not Before):在这之前 token 不可用。和 exp 配合使用,可以控制 token 的精确生效窗口。
  • iss(Issuer):签发者,标识是谁签发了这个 token。多服务架构里用 iss 区分不同的签发方。收到 token 后先检查 iss 是否合法,防止跨系统 token 滥用。
  • aud(Audience):受众,标识这个 token 是给谁用的。例如一个 token 指定了 aud: "api.example.com",就不该被 admin.example.com 接受。aud 可以是字符串也可以是字符串数组。

公共声明是用 JWT 注册表里登记过的字段名,比如 nameemailrole。任何人都可以用,但要注意命名冲突。建议用 URI 格式的命名空间来避免冲突。

私有声明是服务端自定义的字段,比如 departmentpermissions。这是最容易出问题的部分——开发者经常把敏感数据塞进来,忘了 payload 只是编码。密码、身份证号、信用卡号这些绝对不能放。

一个典型的 payload 长这样:

{
  "sub": "user_10086",
  "name": "John Doe",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516325422,
  "iss": "chuanzhang-auth"
}

需要注意一点:iat 和 exp 用的是 Unix 时间戳(秒级),不是毫秒。新手经常传毫秒时间戳导致 token 立即过期或永不过期。

Signature 的工作原理

Signature 是 JWT 防篡改的核心。它的计算方式很简单:

对 Header 和 Payload 分别做 Base64URL 编码,用点号拼起来,然后拿密钥做签名:

signature = HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

用 Node.js 写出来大概是这样:

const crypto = require("crypto");

function createSignature(header, payload, secret) {
  const encodedHeader = base64urlEncode(JSON.stringify(header));
  const encodedPayload = base64urlEncode(JSON.stringify(payload));
  const data = encodedHeader + "." + encodedPayload;
  return crypto.createHmac("sha256", secret).update(data).digest("base64url");
}

function verifySignature(token, secret) {
  const parts = token.split(".");
  if (parts.length !== 3) return false;
  const data = parts[0] + "." + parts[1];
  const expected = crypto
    .createHmac("sha256", secret)
    .update(data)
    .digest("base64url");
  return expected === parts[2];
}

服务端收到 token 后,用同样的密钥和同样的算法重新算一遍签名,比对结果是否一致。一致说明数据没有被篡改,不一致直接拒绝。

这里有个关键点:签名验证只能保证数据没有被改过,不能保证数据是保密的。Payload 是明文编码的,任何人都能读。JWT 的全称里没有 "加密" 二字——它只做签名,不做加密。如果需要加密,应该用 JWE(JSON Web Encryption),那是另一套规范了。

签名的长度取决于算法。HS256 的签名固定 32 字节(Base64URL 编码后约 43 字符)。RS256 的签名长度取决于 RSA 密钥长度,2048 位密钥的签名约 256 字节(编码后约 342 字符)。这意味着 RS256 的 token 比 HS256 大不少。

HS256 和 RS256 怎么选

这两个是最常用的签名算法,但它们的数学原理和使用场景完全不同。

HS256(HMAC with SHA-256) 是对称算法。签名和验证用同一个密钥。谁拿到密钥,谁就能签发和验证 token。

HS256 的优点是速度快、实现简单。缺点是密钥分发是个大问题。假如你有三个微服务:A 签发 token,B 和 C 验证 token。B 和 C 必须持有和 A 一样的密钥。任何一个服务被攻破,密钥就泄露了。更麻烦的是密钥轮换——换密钥时必须所有服务同步更新,哪个节点没换到就全崩了。

HS256 适合单一服务、内部系统、或者你能完全控制所有节点的场景。我的博客后台就用 HS256,一个服务一把密钥,够了。

RS256(RSA with SHA-256) 是非对称算法。用私钥签名,用公钥验证。私钥只由签发方持有,公钥可以公开分发。

RS256 的优点是多了一层隔离。三个微服务的场景下,只有 A 持有私钥。B 和 C 只需要公钥,公钥泄露了也没关系——它只能验证,不能签名。更妙的是,公钥可以通过公开的 JWKS endpoint 获取,客户端或者下游服务可以自动拉取。轮换密钥时,签发方换私钥签新 token,公钥更新一下 JWKS 就行,验证方不需要改代码。

RS256 的缺点是计算速度慢(比 HS256 慢一个数量级),token 体积也更大。HS256 只要做一次 HMAC 哈希,RS256 要做 RSA 模幂运算。对高频 API 的影响不能忽略。

还有 ES256(ECDSA with P-256),它用椭圆曲线算法,速度和 HS256 差不多,但安全性相当于 RSA 3072 位。缺点是实现复杂,不是所有 JWT 库都支持得好。

密钥轮换的建议

不管用哪种算法,密钥都要定期轮换。HS256 的轮换策略是多密钥并行——签发 token 时用新密钥,验证时新旧密钥都试一遍,等旧 token 过期后再移除旧密钥。RS256 更简单:JWKS 里同时保留新旧公钥,新 token 用新私钥签就行。

我的选择标准是这样的:

  • 单体应用、内部工具、开发环境:无脑 HS256。简单够用。
  • 微服务架构、有第三方接入:必须 RS256。密钥隔离是刚需。
  • 移动端和 SPA 客户端验证 token:用 RS256,客户端只需要公钥,不需要持有密钥。
  • 性能敏感的高频 API:HS256 更快。但要确保密钥不泄露。
  • 多团队协作的项目:RS256,公钥可以放在公共的配置中心管理。

HS256 与 RS256 签名算法对比

JWT 和 Session 的区别

JWT 经常被拿来和传统的 Session 认证做对比。两者思路完全不同。

Session 方案:用户登录后,服务端生成一个 session ID 存在内存或 Redis 里,同时把 session ID 通过 cookie 返回给客户端。客户端每次请求带上 cookie,服务端查 session 数据。

优点是服务端可以随时销毁 session(比如封号、踢人下线)。缺点是服务端需要存状态——分布式环境下要用 Redis 共享 session,否则不同节点的 session 不互通。用户量大了以后,Redis 的内存占用也是个成本。

JWT 方案:用户登录后,服务端签发一个 JWT,客户端保存。客户端每次请求在 Authorization 头里带上 JWT,服务端验签通过就信任数据。

优点是服务端不需要存 session,天然适合分布式和无状态架构。缺点是一旦签发,在过期之前无法撤销。如果用户改了密码,老 token 仍然有效直到过期。这意味着你不能踢人下线——除非额外维护一个黑名单。

性能对比

Session 每次请求都要查数据库或 Redis,多了一次网络开销。JWT 验签不需要外部存储,纯 CPU 计算,延迟更低。但 JWT 的签名验证也不是免费的——RS256 的一次验签大约需要 0.5-2 毫秒,高频请求下累计不可忽视。

token 体积也是考虑因素。Session 方案在 cookie 里只传一个几十字节的 session ID。JWT 方案每次请求都带几百字节甚至几千字节的 token,在弱网环境下影响更明显。

具体怎么选?

如果你的系统需要即时撤销权限(比如支付系统、后台管理系统),Session 更合适。如果你做的是开放的 API 服务、微服务架构,或者不想维护 session 存储,JWT 更合适。

也有一些混合方案:用 JWT 做身份认证,同时维护一个短期的黑名单存在 Redis 里,存已撤销但未过期的 token。把两边的优势结合起来。GitHub 的 API 就用了类似的策略——token 本身用 JWT 格式,但在服务端保留撤销能力。

JWT 认证流程:客户端与服务端交互

Token 应该存哪里

JWT 拿到之后存在哪里,是个争议很大的话题。每次讨论都能吵几十层楼。

localStorage / sessionStorage:实现最简单。前端代码直接从 storage 里取出来塞到请求头就行。但问题是 storage 里的数据可以被同一域名下的任何 JavaScript 访问。如果你的站点被 XSS 攻击,攻击者可以直接偷走 token。XSS 防护是这种方案的命门——一个未转义的用户输入就能让整个认证体系失效。

HttpOnly Cookie:设置了 HttpOnly 标记的 cookie 不能被 JavaScript 读取,XSS 攻击拿不到。但 CSRF(跨站请求伪造)是个隐患——攻击者可以诱导用户浏览器自动带上 cookie 发起恶意请求。需要配合 SameSite 属性(SameSite=Strict 或 SameSite=Lax)以及 CSRF token 来防御。好消息是现代浏览器默认 SameSite=Lax,大大缓解了 CSRF 问题。

内存变量:把 token 存在 JavaScript 变量里(比如 React 的 state、Vuex/Pinia)。这种方式最安全,XSS 拿不到,CSRF 打不了。但用户刷新页面后 token 就丢了,必须重新获取。体验上的代价是每次刷新都要重新认证或走刷新流程。

Service Worker 方案:把 token 放在 Service Worker 的缓存里,页面脚本通过 postMessage 获取。Service Worker 的环境隔离性比页面脚本好,XSS 更难触及。但实现复杂,而且 Service Worker 在某些浏览器上有兼容性问题。

我的做法是,前端 SPA 场景:access token 存在内存变量里,同时用 HttpOnly cookie 存 refresh token。页面加载时用 refresh token 通过一个专门的接口换取新的 access token。这样 access token 每次刷新页面都会重新获取,不落盘。refresh token 有 HttpOnly 保护,XSS 够不到。用 SameSite=Strict 防御 CSRF。缺点是实现复杂,需要维护刷新逻辑和 token 过期队列。

小项目或者内部系统:localStorage 存着就行。只要做好 XSS 防护(转义用户输入、设置 CSP 头),问题不大。别为了"绝对安全"把架构搞得没法维护。安全是成本,要根据项目阶段做取舍。

Token 过期了怎么办

JWT 一旦签发就不能修改,所以过期只能重新签发。这就引出了刷新机制。

最简单的方案:把 exp 设长一些(比如 7 天),用户过期了重新登录。用户体验很差,但实现最简单。内部工具用这个方案没问题,面向用户的产品就算了。

大部分项目用双 Token 方案:

  • Access Token:短期有效(15 分钟到 1 小时)。用来访问 API。
  • Refresh Token:长期有效(7 天到 30 天)。专门用来换取新的 Access Token。

客户端发现 Access Token 过期(API 返回 401),就用 Refresh Token 去请求新的 Access Token,全程无感刷新。前端拦截器处理这个逻辑:

// axios 请求拦截器
let isRefreshing = false;
let refreshQueue = [];

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const { config, response } = error;
    if (response?.status !== 401 || config._retry) {
      return Promise.reject(error);
    }
    config._retry = true;
    if (!isRefreshing) {
      isRefreshing = true;
      try {
        const { accessToken } = await refreshToken(refreshTokenValue);
        localStorage.setItem("access_token", accessToken);
        refreshQueue.forEach((resolve) => resolve(accessToken));
        refreshQueue = [];
        config.headers.Authorization = "Bearer " + accessToken;
        return api(config);
      } catch {
        refreshQueue.forEach((reject) => reject(error));
        refreshQueue = [];
        // redirect to login
      } finally {
        isRefreshing = false;
      }
    } else {
      return new Promise((resolve, reject) => {
        refreshQueue.push((token) => {
          config.headers.Authorization = "Bearer " + token;
          resolve(api(config));
        });
      });
    }
  },
);

这段代码处理了一个边缘情况——多个请求同时失败时,只发一次刷新请求,其他请求排队等新的 token。

Refresh Token 的风险更大(有效期长),需要更严格保护。通常存在 HttpOnly cookie 里,而且服务端可以撤销 Refresh Token(存在数据库或 Redis 里)。这样即使泄露,管理员可以让它失效。

关于刷新机制,不同团队有不同做法。有些人用 "重新签发"(每次刷新都发一个新 token),有些人用 "滑动过期"(每次访问都延长 token 有效期,像 Session 一样)。两者各有优劣,我倾向于重新签发——逻辑清晰,容易追踪。

Auth0 的实现值得参考:它们把 Refresh Token Rotation 作为推荐实践——每次使用 Refresh Token 后,不仅返回新的 Access Token,连 Refresh Token 本身也换一个新的。旧的 Refresh Token 立即作废。这样即使 Refresh Token 泄露,一旦被使用一次就不能再用了。

常见漏洞

JWT 安全方面的漏洞很多是规范本身留下的坑。我整理几个最常见的。

alg 设为 none

这是最经典的 JWT 攻击。攻击者把 header 里的 alg 改成 none,删掉 signature 部分,服务端如果直接用 none 算法验证,永远不会验证签名。有 JWT 库在早期版本里默认接受 none 算法。这个漏洞在 2015 年被广泛披露,但至今仍有新项目踩坑。

防御方法:服务端强制指定允许的算法列表,拒绝 none。token 验签前先检查 alg 是否在允许列表里。

密钥混淆攻击

当服务端同时支持 HS256 和 RS256 时,攻击者拿到公钥后(公钥是公开的),把 header 的 alg 从 RS256 改成 HS256,用公钥作为 HS256 的密钥来签名。如果服务端用公钥作为 HS256 密钥来验证,攻击就成功了。

听起来不可思议,但现实中发生过。知名的 JWT 库 jjwt 和 node-jsonwebtoken 都出过类似问题。防御方法也是固定算法列表,或者算法和密钥一一对应绑定。

不检查 exp

很多开发者忘了检查 exp 字段。没有过期检查的 JWT 等于永久有效的万能钥匙。有一个知名 SaaS 服务早期版本的 API 不检查 exp,导致泄露的 token 可以无限期使用。JWT 库默认不会检查 exp,需要开发者显式调用验证方法。比如 jsonwebtoken 库要用 jwt.verify(token, secret, { algorithms: ['HS256'] }) 这样调用才会校验。

签名绕过 via 弱密钥

HS256 的安全性完全取决于密钥强度。用 "secret" 或者 "123456" 当密钥,等于没锁门。攻击者可以尝试常见密钥列表暴力破解 token 的签名。方法很简单:用常见密钥列表去算 HMAC,看哪个密钥能算出一致的签名。

防御方法是用足够长且随机的密钥。至少 32 字节(256 位),用密码学安全的随机数生成器产生。不要自己发明密钥派生函数。

签名绕过 via 不安全的比较

如果服务端在验证签名时用了不安全的比较(比如字符串比较而非固定时间比较),可能被 timing attack 绕过。不过 JWT 库通常已经处理了这个问题,升级到最新版本就行。

JWK Header Injection

有些 JWT 库支持从 header 里的 jwk 字段直接拿公钥验证。攻击者可以自己生成一对密钥,把公钥塞进 jwk 字段,然后用私钥签名。如果服务端信任 header 里的 jwk,攻击就成功了。这个漏洞在 2018 年被发现,影响了很多流行 JWT 库。升级到最新版本基本都能修复。

所有漏洞归结起来就一句话:不要信任用户发来的 JWT。Header 里的字段要检查,签名要验证,过期要判断。写代码时假设攻击者已经拿到了你的 token 样本在逆向分析。

常见 JWT 库和实现选型

不同语言有各自的 JWT 库,用法大同小异但细节有区别。

Node.js:最常用的是 jsonwebtoken(npm 周下载量两千万)。它支持 HS256、RS256、ES256,API 设计简洁。签发一个 token 只需要两行:

const jwt = require("jsonwebtoken");
const token = jwt.sign({ sub: "user_10086", role: "admin" }, secret, {
  expiresIn: "1h",
});

Java:主流选择是 jjwt(Java JWT)和 nimbus-jose-jwt。Spring Boot 项目推荐 spring-security-oauth2-jose,它封装好了 JWKS 和 JWT 验证的集成。

Gogolang-jwt/jwt 是事实标准。Go 的 JWT 库比其他语言更强调类型安全——claims 用结构体定义,不像 JavaScript 那么随意。

PythonPyJWT 最轻量,python-jose 功能更全(支持更多算法)。Django 项目会用 djangorestframework-simplejwt

跨语言需要注意的坑

一是算法命名。Node.js 的 RS256 在 Java 里是 SHA256withRSA,在 Go 里是 RS256(但内部实现不同)。不确认的话去 jwt.io 验一下。

二是密钥格式。RS256 的私钥和公钥通常是 PEM 格式,不同库对 PEM 的解析要求不同。换行符、头部标记、Base64 编码的细微差别都可能导致解析失败。我遇到过 PEM 文件里多了一个空格字符,Node.js 能解析但 Java 报错的问题。

三是时间精度。大多数库用秒级时间戳,少数用毫秒。跨语言互调时最好统一按 RFC 7519 推荐的秒级。

生产环境 JWT 验证清单

整理了一份 checklist。如果上线 JWT 相关功能都会过一遍:

  • alg 是否硬编码在服务端配置里,而不是从 token 的 header 里读取
  • 是否显式拒绝了 none 算法
  • exp 字段是否在每次请求时被验证
  • iss 和 aud 是否做了校验
  • 密钥长度是否足够(HS256 至少 32 字节)
  • 密钥是否存放在环境变量或密钥管理服务中,而不是代码仓库里
  • 是否限制了 token 的最大长度(防止 payload 过大导致 DOS)
  • 刷新 token 时是否做了旧 token 失效处理
  • 登录改密后是否清除了旧的 refresh token
  • 日志里是否记录了 token 验证失败的原因(算法不匹配、过期、签名无效)

最后一条很多人忽略。不记日志,线上出了 401 你根本不知道是哪个环节出了问题。

JWT 的尺寸问题

JWT 不是越小越好,但太大确实有影响。HTTP 请求头的大小限制各家不同——Nginx 默认 8KB,IIS 是 16KB。超过限制请求会被拒绝。

一个典型的 HS256 的 JWT 大概 400-600 字节。如果 payload 里塞了很多自定义字段,轻松超过 1KB。每次请求都要带这个头,对带宽和延迟都是负担。

之前在项目里见过有人把用户的完整权限列表塞进 JWT payload,包括几十个 API 端点的权限标记。token 超过 3KB,每次请求多传 3KB 数据。后来换成了用 JWT 只存用户 ID 和角色,权限信息用单独的接口按需获取,token 体积降到 500 字节左右。

如果你的 token 超过 2KB,建议检查 payload 里是不是放了不该放的东西。

JWT 在 OAuth2 中的角色

很多人搞混 JWT 和 OAuth2 的关系。简单说:JWT 是一种令牌格式,OAuth2 是一种授权框架。两者可以独立使用,但组合在一起非常强大。

在 OAuth2 流程中,授权服务器签发的 Access Token 可以是 JWT 格式。这时候 JWT 的声明里会包含授权相关的信息,比如 scope、client_id、authorization_details。

OAuth2 还用到了另一种特殊的 JWT——JWT Profile for Client Authentication。客户端可以用 JWT 来向授权服务器证明自己的身份,代替传统的 client_secret 方式。这在没有浏览器重定向的机器对机器场景里很常见。

还有一个概念叫 JWKS(JSON Web Key Set)。它是一个端点,返回授权服务器的公钥列表。客户端拿到公钥后可以验证 Access Token 的签名。JWKS 是 RS256 在 OAuth2 和 OpenID Connect 中广泛使用的基础。Google、GitHub、Auth0 都有自己的 JWKS endpoint。

OpenID Connect(OIDC)基于 OAuth2,引入了 ID Token 的概念。ID Token 固定是 JWT 格式,包含用户的身份信息。做单点登录时一定会遇到它。你在 "使用 Google 登录" 时拿到的那个 id_token 就是 JWT,解码后能看到你的邮箱、姓名、头像地址。

如果你觉得这些概念绕,很正常。OAuth2 和 OIDC 的规范加起来几百页。但搞清楚 JWT 在其中扮演的角色——一种自包含的令牌格式——就够了。

用工具调试 JWT

写代码时遇到 JWT 相关问题,要学会借助工具JWT 解析工具 可以快速解析 token 的三部分内容,自动检测过期状态。Header 和 Payload 以格式化 JSON 展示,Signature 单独列出。还可以用Base64 编解码工具来验证 Base64URL 编码结果,手动调试编码问题时很有用。

有时候我也会用命令行来调试:

# 解码 JWT 的 Payload(Mac / Linux)
echo 'eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiSm9obiJ9' | base64 -d 2>/dev/null || echo 'need to replace - _ with + / and add padding'

# 验证签名(需要 jwt-cli 工具)
jwt decode <token>