2 分钟

什么是 JWT?JSON Web Token 清晰指南

了解什么是 JWT(JSON Web Token)、它的三部分如何工作、常见用途,以及避免常见令牌错误的关键安全建议。

什么是 JWT?JSON Web Token 清晰指南

用通俗话说的 JWT

一个 JWT(JSON Web Token) 是一个紧凑的、URL 安全的字符串,用于表示一组信息(通常关于用户或会话),可以在系统之间传递。你通常会看到它以类似 eyJ... 开头的长值形式出现,发送时常放在 HTTP 头里,例如 Authorization: Bearer <token>

为什么要使用令牌?

传统登录通常依赖 服务器会话:你登录后服务器保存会话数据并给浏览器一个会话 ID 的 cookie。每个请求都会携带该 cookie,服务器查找会话。

使用 基于令牌的认证,服务器可以避免为每个用户请求保留会话状态。相反,客户端持有一个令牌(如 JWT)并在调用 API 时附带它。这在 API 场景下很流行,因为它:

  • 在多个服务间(API 网关、微服务)工作良好
  • 适合直接调用 API 的移动应用和单页应用(SPA)
  • 减少跨服务器共享会话存储的需要

重要的细微差别: “无状态”并不意味着“永远不做服务器端检查”。许多真实系统仍会根据用户状态、轮换密钥或撤销机制来验证令牌。

认证 vs 授权(通俗解释)

  • 认证(Authentication) 回答:你是谁?(你登录并证明身份。)
  • 授权(Authorization) 回答:你被允许做什么?(你可以读取发票、编辑项目、访问管理员页面等。)

JWT 常携带 认证证明(你已登录)和一些基本的 授权提示(角色、权限、scope),但服务器仍需执行完整的授权规则。

JWT 会出现在哪些场景

你通常会看到 JWT 用作 访问令牌 在:

  • Web API
  • SPA
  • 移动应用
  • 使用 OAuth 2.0 或 OpenID Connect(OIDC)的系统

JWT 结构:头部、载荷和签名

一个 JWT 是由 三部分 组成的紧凑字符串,每部分都经过 base64url 编码 并用点分隔:

header.payload.signature

示例(已删减):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNzAwMDAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c…

1) 头部(Header)

头部 描述令牌如何被创建——最重要的是 签名算法(例如 HS256、RS256/ES256)和令牌类型。

常见字段:

  • typ:通常是 "JWT"(在实践中常被忽略)
  • alg:使用的签名算法
  • kid:密钥标识符,帮助验证方在密钥轮换时选择正确的密钥

安全提示:不要盲目信任头部。应强制允许列表(allowlist)中的算法,并且不要接受 alg: "none"

2) 载荷(Payload)

载荷 存放关于用户和令牌上下文的“声明”(字段):令牌是谁的、由谁签发、何时过期等。

重要:JWT 默认不是加密的。 Base64url 编码只是让令牌 URL 安全;它并不隐藏数据。任何拿到令牌的人都可以解码头部和载荷。

因此应避免把秘密(密码、API 密钥)或敏感个人数据放入 JWT。

3) 签名(Signature)

签名 是对头部 + 载荷 使用密钥签名生成的:

  • HS256:用共享密钥进行签名和验证
  • RS256/ES256:私钥签名;公钥验证

签名提供了 完整性:服务器可以验证令牌没有被修改并且由受信任的签发方生成。它不提供机密性。

大小考虑

因为 JWT 在每次发送时都包含头部和载荷,令牌越大意味着带宽和开销越大。保持声明精简,优先使用标识符而不是大块数据。

载荷与声明:可以存什么(以及不该存什么)

自信设计声明
在生成代码前规划角色、受众、颁发者和过期规则。

声明大体分为两类:注册声明(标准名)和 自定义声明(你应用的字段)。

常见的注册声明

  • iss(签发者):谁创建了令牌
  • sub(主体):令牌关于谁(通常是用户 ID)
  • aud(受众):令牌面向谁(例如特定 API)
  • exp(到期时间):令牌何时应停止被接受
  • iat(签发时间):令牌何时创建
  • nbf(不可用于此前):令牌在该时间之前不应被接受

自定义声明:保持最小化

仅包含接收方真正需要用于授权决策的信息。

合适的例子:

  • 稳定的内部用户标识(user_id
  • 精简的角色/权限集(仅在能保持最新时)
  • 多租户应用中的租户/组织 ID

避免那些“方便但臃肿”的声明:它们会膨胀令牌、快速过时,并在令牌泄露时增加影响范围。

绝不要把这些放入 JWT 载荷

因为载荷是可读的,不要存放:

  • 密码、API 密钥、刷新令牌或任何秘密值
  • 支付信息、政府 ID 或敏感个人数据
  • 任何不希望被从浏览器、代理或日志中复制的内容

如果需要敏感信息,请把它保存在服务器端,并在令牌里只放引用(如 ID),或者在适当时使用加密令牌格式(JWE)。

签名如何工作(以及它保证了什么)

签名不是加密。

  • 签名 像给信封封口:别人可以读取信件内容,但能验证信件未被篡改。
  • 加密 像把信件锁在盒子里:只有持有钥匙的人才能读取。

当 JWT 被签发时,服务器对编码后的头部 + 载荷进行签名。令牌被提交时,服务器重新计算签名并比较。如果有人改动哪怕一个字符(例如把 "role":"user" 改为 "role":"admin"),验证会失败,令牌会被拒绝。

JWT vs OAuth、OpenID Connect 与令牌类型

JWT 是一种 令牌格式。OAuth 2.0 与 OpenID Connect(OIDC)是描述应用如何请求、签发和使用令牌的 协议

OAuth 2.0 与访问/刷新令牌

OAuth 2.0 主要处理 授权:让应用在不共享用户密码的情况下代表用户访问 API。

  • 访问令牌:用于向 API 证明权限;可以是 JWT 或不透明令牌
  • 刷新令牌:用于获取新的访问令牌

访问令牌通常 短期有效(几分钟)。短生命周期能限制令牌泄露时的损害。

OpenID Connect(OIDC)与 ID 令牌

OIDC 在 OAuth 2.0 之上增加了 认证(用户是谁),并引入了通常为 JWT 的 ID 令牌

  • ID 令牌:供 客户端应用 确认用户身份
  • 访问令牌:供 API 授权请求

关键规则:不要用 ID 令牌来调用 API

如果想了解更多实践流程,请参见 /blog/jwt-authentication-flow。

常见的 JWT 认证流程

掌控你的实现
配置就绪后导出源码,保持掌控。

一个典型流程如下:

1) 登录

用户登录(邮箱/密码、SSO 等)。成功后,服务器创建一个 JWT(通常是访问令牌),包含如主体和到期时间等必要声明。

2) 令牌签发

服务器签名令牌并将其返回给客户端(Web 应用、移动应用或其他服务)。

3) 调用 API

对于受保护的端点,客户端在 Authorization 头中包含 JWT:

Authorization: Bearer <JWT>

4) 验证

在响应请求前,API 通常会检查:

  • 签名(完整性 + 受信任的签发方)
  • exp(未过期)
  • iss(预期的签发者)
  • aud(面向本 API)

如果所有检查通过,API 将把该用户视为已认证并执行授权规则(例如记录级权限)。

5) 关于时钟偏差的快速说明

因为系统时钟会漂移,许多系统在验证基于时间的声明(如 exp 和有时的 nbf)时允许小幅 时钟偏差(clock skew)。将偏差保持较小,以避免无意间延长令牌有效期。

在何处安全存储 JWT

存储选择会改变攻击者能窃取的目标以及他们重放令牌的难易程度。

浏览器应用:内存 vs localStorage vs cookies

内存存储(通常推荐用于 SPA) 将访问令牌保存在 JS 状态中。刷新页面会清除它,减少“事后抓取”风险,但 XSS 漏洞仍可在页面运行时读取它。配合短期访问令牌和刷新流程使用。

localStorage/sessionStorage 使用方便但风险较高:任何 XSS 漏洞都能将令牌导出。如果使用,必须把 XSS 防护作为非可选(使用 CSP、输出转义、依赖库卫生),并保持令牌短期有效。

安全 Cookie(通常是 Web 的默认更安全选项) 将令牌存放在 HttpOnly cookie 中,JavaScript 无法读取,从而降低 XSS 导致的令牌窃取风险。代价是会带来 CSRF 风险,因为浏览器会自动附带 Cookie。

如果使用 Cookie,请设置:

  • HttpOnly
  • Secure(仅限 HTTPS)
  • SameSite=LaxSameSite=Strict(某些跨站流程可能需要 SameSite=None; Secure

并考虑对状态变更请求使用 CSRF 令牌。

移动应用:优先使用操作系统的安全存储

在 iOS/Android 上,应将令牌存放在平台的安全存储中(Keychain / Keystore 支持的存储)。避免放在明文文件或普通偏好设置中。如果你的威胁模型包含已越狱/Root 的设备,则假定可能被提取,并依赖短期令牌与服务器端控制来降低风险。

最小权限原则

限制令牌的能力:使用最小 scope/声明、保持访问令牌短期有效,并避免把敏感数据嵌入令牌中。

常见的 JWT 安全陷阱(要避免)

JWT 很便利,但许多事故来自可预见的错误。把 JWT 当作现金:谁得到就可能花用它。

1) 过长的过期时间

如果令牌有效期是几天或几周,一旦泄露攻击者就有整段时间可利用。

优先使用短期访问令牌(分钟级),并通过更安全的机制刷新。需要“记住我”功能时,用刷新令牌和服务器端控制实现。

2) 跳过签发者和受众检查

有效签名不足够。请验证 issaud,并校验时间相关声明如 expnbf

3) 信任已解码的载荷

解码不等于验证。始终在服务器端验证签名并执行权限检查。

4) 算法混淆与密钥混用

  • 不要接受令牌声称使用的任意算法。白名单预期算法。
  • 不要混淆对称密钥(HS256)与公/私钥(RS256/ES256)。
  • 通过为不同环境分离密钥并轮换密钥来最小化影响范围。

5) 令牌通过 URL 泄露

避免把 JWT 放在查询参数中。它们可能出现在浏览器历史、服务器日志、分析工具和 Referrer 头中。

使用 Authorization: Bearer ... 代替。

6) 没有密钥轮换或撤销计划

假定密钥和令牌可能泄露。轮换签名密钥,使用 kid 支持平滑轮换,并制定撤销策略(短期过期 + 能禁用帐户/会话)。关于存储建议,请参见 /blog/where-to-store-jwts-safely。

何时使用 JWT(以及何时不要)

跳过 JWT 样板代码
让 Koder.ai 处理重复的连接工作,你专注于密钥轮换和策略。

JWT 很有用,但并不总是最佳选择。关键问题是你是否受益于一种自包含令牌,它可以在不进行每次请求数据库查验的情况下被验证。

适合使用 JWT 的场景

  • 大规模无状态 API:本地验证(签名 + 到期)而无需每次请求查找会话
  • 多服务 / 微服务:共享验证规则和公钥
  • SPA 与移动应用:客户端直接调用 API
  • 短期访问令牌:降低被盗用的影响

不适合使用 JWT 的场景

  • 必须实现即时撤销:如果需要“立即在所有地方登出”,会话更简单(无需额外基础设施)
  • 需要携带敏感数据:典型 JWT 是签名的,不是加密的
  • 长期有效令牌:它们价值高,值得被窃取

对于传统的服务器渲染 Web 应用,如果需要简单的失效控制,服务器端会话 + HttpOnly Cookie 通常是更简单、更安全的默认选择。

快速决策清单

如果你需要跨服务的无状态验证并且能保持令牌短期有效,就选 JWT。反之如果需要即时撤销、计划把敏感数据放在令牌里,或者可以无痛使用会话 Cookie,就避免使用 JWT。

实用核对清单与常见问答

验证核对清单(每次都要检查的项)

  1. 签名有效

使用正确的密钥和预期算法验证。拒绝无效签名——没有例外。

  1. exp(到期)

确保令牌未过期。

  1. nbf(不可用时间之前)

如存在,确保令牌尚不可提前使用。

  1. aud(受众)

确认令牌是针对 你的 API/服务 的。

  1. iss(签发者)

确认令牌来自预期的签发者。

  1. 健全性检查(推荐)

验证令牌格式、强制最大大小,并拒绝意外的声明类型以减少边界情况错误。

选 HS256 还是 RS256/ES256

  • HS256(对称密钥):一个共享秘密用于签名和验证。

    • 适合:由单一团队控制的单个应用/API。
    • 风险:任何能验证的角色也能伪造令牌(因为有同一个密钥)。
  • RS256 / ES256(非对称密钥):私钥签名;公钥验证。

    • 适合:多个服务需要验证令牌,可分发公钥而不允许签名。
    • 运行注意:轮换通常更安全,因为只有签发方持有私钥。

经验法则:如果有多个独立系统需要验证令牌(或你不完全信任每个验证方),优先选择 RS256/ES256

监控与日志(在不泄露令牌的情况下)

  • 不要记录原始令牌(包括头部、Cookie、查询字符串)。
  • 如果需要关联,记录 令牌指纹(例如哈希)或安全的元数据(issaud,以及在策略允许时的用户 ID)。
  • 监控异常:签名失败、过期令牌激增、异常的受众/签发者和可疑的刷新模式。

常见问题(FAQs)

JWT 是加密的吗?

默认不是。大多数 JWT 是签名的,不是加密的,这意味着内容任何拿到令牌的人都能读取。使用 JWE 或把敏感数据放在服务器端以保证机密性。

我能撤销 JWT 吗?

如果仅依赖自包含的访问令牌,撤销并不容易。常见做法包括短期访问令牌、高风险事件的 deny-list、或带轮换的刷新令牌。

exp 应该设多长?

尽可能短,同时兼顾用户体验与架构需求。许多 API 使用分钟级的访问令牌,配合刷新令牌实现长会话。

使用 Koder.ai 快速构建受 JWT 保护的应用

如果你在一个新 API 或 SPA 中实现 JWT 认证,很多工作是重复性的:接入中间件、验证 iss/aud/exp、设置 Cookie 标志、避免把令牌写入日志等。

使用 Koder.ai,你可以通过对话驱动工作流快速生成 Web 应用(React)、后端服务(Go + PostgreSQL)或 Flutter 移动应用——在 规划模式 中迭代,使用 快照与回滚 精炼安全设置,并在准备就绪时导出源码。这能帮助你加速实现基于 JWT 的认证流程,同时保留对验证逻辑、密钥轮换策略和部署/托管设置(含自定义域名)的控制。

常见问题

什么是 JWT,通常把它放在哪发送?

A JWT (JSON Web Token) 是一个紧凑、URL 安全的字符串,携带声明(数据字段),服务器可以验证它。它常在 API 请求中通过以下方式发送:

  • Authorization: Bearer <token>

关键点:服务器可以验证令牌的完整性(通过签名),而不需要为每个请求保留单独的会话记录。

JWT 认证与服务器会话有什么不同?

会话认证通常在服务器端保存状态(通过会话记录,用 cookie/session ID 作为键)。使用 JWT 认证时,客户端每次请求携带一个签名的令牌,API 验证该令牌。

JWT 在 API 和多服务架构中很受欢迎,因为验证可以本地完成,减少对共享会话存储的需求。

“无状态”并不意味着没有服务器端校验:很多系统仍会包含撤销列表、用户状态检查或密钥轮换等服务器端检查。

JWT 的三部分(头部、载荷、签名)是什么?

一个 JWT 由三部分 Base64URL 编码并以点分隔:

  • header.payload.signature

header 描述签名方式,payload 包含声明(比如 sub, exp, aud),signature 用于检测篡改。

JWT 是加密的吗?拿到后能看到里面的内容吗?

不是。标准 JWT 通常是签名的,而不是加密的。

  • 签名用于证明完整性(未被篡改)和来源可信性(由受信任的签发方签发)。
  • 任何获得令牌的人都可以 Base64URL 解码并读取 header 和 payload。

如果需要机密性,请考虑使用 JWE(加密令牌),或将敏感数据保留在服务器端,只在 JWT 中放置标识符。

JWT 的签名能保证什么,不能保证什么?

签名让服务器能够验证令牌未被修改,并且由拥有签名密钥的一方签发。

它并不:

  • 隐藏载荷内容
  • 证明用户仍然处于激活状态(除非你额外检查)
  • exp 前自动撤销令牌

把令牌视为凭证:一旦泄露,通常可以在其有效期内被重放使用。

`alg` 和 `kid` 在 JWT 头部是什么意思,为什么重要?

alg 告诉验证方使用了哪种算法(例如 HS256 或 RS256)。kid 是密钥标识符,有助于在密钥轮换时选择正确的验证密钥。

安全建议:

  • 白名单预期的算法;不要接受任意 alg 值。
  • 绝不接受 alg: "none"
  • 不要让不可信的 kid 导致不安全的密钥查找行为。
我应该在 JWT 的载荷中包含哪些声明?

从标准声明开始,尽量让自定义声明最小化。

常见注册声明:

  • iss(签发者)
  • sub(主体 / 用户标识)
  • aud(受众 / 目标 API)
  • exp(到期时间)
  • iat(签发时间)
  • nbf(不可用时间之前)

避免在载荷中放入秘密或敏感的个人数据,因为一旦令牌暴露这些内容就可被读取。

JWT、OAuth 2.0 和 OpenID Connect 有什么关系(访问令牌 vs ID 令牌)?

JWT 是一种令牌格式;OAuth 2.0 和 OpenID Connect 是协议

常见对应关系:

  • Access token(访问令牌):用于调用 API(可以是 JWT 或不透明令牌)。
  • ID token(OIDC):用于客户端确认用户身份(通常是 JWT)。
  • Refresh token(刷新令牌):用来换取新的访问令牌(通常是不透明的;高度敏感)。

重要规则:不要把 ID token 用来直接调用 API,即便它看起来像访问令牌。

在浏览器应用中应该如何安全地存储 JWT?

针对浏览器应用,常见存储选项:

  • 内存:减少“事后偷取”风险,但在页面运行时若发生 XSS 仍可被读取。
  • localStorage/sessionStorage:方便,但任意 XSS 都能导出令牌。如果使用,必须把 XSS 防护视为不可妥协(CSP、输出转义、依赖卫生),并尽量让令牌短期有效。
  • HttpOnly Secure Cookie:通常对 Web 更安全,因为 JS 无法读取,但带来 CSRF 风险(浏览器会自动附带 cookie)。

如果使用 Cookie,请设置:

  • HttpOnly
  • Secure(仅 HTTPS)
  • SameSite=LaxSameSite=Strict(某些跨站流程可能需要 SameSite=None; Secure

此外,对于状态变更请求考虑使用 CSRF 令牌。

无论哪种方式,都应让访问令牌短期有效并最小化权限。

我的 API 在验证 JWT 时应该做哪些检查?

至少要验证:

  • 签名(使用正确的密钥和白名单算法)
  • exp(未过期)
  • iss(来自预期的签发者)
  • aud(面向你的 API/服务)
  • nbf(若存在,确保不提前使用)

另外添加实用的防护:

  • 强制最大令牌大小
  • 拒绝意外的声明类型
  • 允许小幅时钟偏差以应对时间漂移问题

Related posts