什么是 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,请设置:
HttpOnlySecure(仅限 HTTPS)SameSite=Lax或SameSite=Strict(某些跨站流程可能需要SameSite=None; Secure)
并考虑对状态变更请求使用 CSRF 令牌。
移动应用:优先使用操作系统的安全存储
在 iOS/Android 上,应将令牌存放在平台的安全存储中(Keychain / Keystore 支持的存储)。避免放在明文文件或普通偏好设置中。如果你的威胁模型包含已越狱/Root 的设备,则假定可能被提取,并依赖短期令牌与服务器端控制来降低风险。
最小权限原则
限制令牌的能力:使用最小 scope/声明、保持访问令牌短期有效,并避免把敏感数据嵌入令牌中。
常见的 JWT 安全陷阱(要避免)
JWT 很便利,但许多事故来自可预见的错误。把 JWT 当作现金:谁得到就可能花用它。
1) 过长的过期时间
如果令牌有效期是几天或几周,一旦泄露攻击者就有整段时间可利用。
优先使用短期访问令牌(分钟级),并通过更安全的机制刷新。需要“记住我”功能时,用刷新令牌和服务器端控制实现。
2) 跳过签发者和受众检查
有效签名不足够。请验证 iss 和 aud,并校验时间相关声明如 exp 和 nbf。
3) 信任已解码的载荷
解码不等于验证。始终在服务器端验证签名并执行权限检查。
4) 算法混淆与密钥混用
- 不要接受令牌声称使用的任意算法。白名单预期算法。
- 不要混淆对称密钥(HS256)与公/私钥(RS256/ES256)。
- 通过为不同环境分离密钥并轮换密钥来最小化影响范围。
5) 令牌通过 URL 泄露
避免把 JWT 放在查询参数中。它们可能出现在浏览器历史、服务器日志、分析工具和 Referrer 头中。
使用 Authorization: Bearer ... 代替。
6) 没有密钥轮换或撤销计划
假定密钥和令牌可能泄露。轮换签名密钥,使用 kid 支持平滑轮换,并制定撤销策略(短期过期 + 能禁用帐户/会话)。关于存储建议,请参见 /blog/where-to-store-jwts-safely。
何时使用 JWT(以及何时不要)
JWT 很有用,但并不总是最佳选择。关键问题是你是否受益于一种自包含令牌,它可以在不进行每次请求数据库查验的情况下被验证。
适合使用 JWT 的场景
- 大规模无状态 API:本地验证(签名 + 到期)而无需每次请求查找会话
- 多服务 / 微服务:共享验证规则和公钥
- SPA 与移动应用:客户端直接调用 API
- 短期访问令牌:降低被盗用的影响
不适合使用 JWT 的场景
- 必须实现即时撤销:如果需要“立即在所有地方登出”,会话更简单(无需额外基础设施)
- 需要携带敏感数据:典型 JWT 是签名的,不是加密的
- 长期有效令牌:它们价值高,值得被窃取
何时使用简单的会话 Cookie 更好
对于传统的服务器渲染 Web 应用,如果需要简单的失效控制,服务器端会话 + HttpOnly Cookie 通常是更简单、更安全的默认选择。
快速决策清单
如果你需要跨服务的无状态验证并且能保持令牌短期有效,就选 JWT。反之如果需要即时撤销、计划把敏感数据放在令牌里,或者可以无痛使用会话 Cookie,就避免使用 JWT。
实用核对清单与常见问答
验证核对清单(每次都要检查的项)
- 签名有效
使用正确的密钥和预期算法验证。拒绝无效签名——没有例外。
exp(到期)
确保令牌未过期。
nbf(不可用时间之前)
如存在,确保令牌尚不可提前使用。
aud(受众)
确认令牌是针对 你的 API/服务 的。
iss(签发者)
确认令牌来自预期的签发者。
- 健全性检查(推荐)
验证令牌格式、强制最大大小,并拒绝意外的声明类型以减少边界情况错误。
选 HS256 还是 RS256/ES256
-
HS256(对称密钥):一个共享秘密用于签名和验证。
- 适合:由单一团队控制的单个应用/API。
- 风险:任何能验证的角色也能伪造令牌(因为有同一个密钥)。
-
RS256 / ES256(非对称密钥):私钥签名;公钥验证。
- 适合:多个服务需要验证令牌,可分发公钥而不允许签名。
- 运行注意:轮换通常更安全,因为只有签发方持有私钥。
经验法则:如果有多个独立系统需要验证令牌(或你不完全信任每个验证方),优先选择 RS256/ES256。
监控与日志(在不泄露令牌的情况下)
- 不要记录原始令牌(包括头部、Cookie、查询字符串)。
- 如果需要关联,记录 令牌指纹(例如哈希)或安全的元数据(
iss、aud,以及在策略允许时的用户 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,请设置:
HttpOnlySecure(仅 HTTPS)SameSite=Lax或SameSite=Strict(某些跨站流程可能需要SameSite=None; Secure)
此外,对于状态变更请求考虑使用 CSRF 令牌。
无论哪种方式,都应让访问令牌短期有效并最小化权限。
我的 API 在验证 JWT 时应该做哪些检查?
至少要验证:
- 签名(使用正确的密钥和白名单算法)
exp(未过期)iss(来自预期的签发者)aud(面向你的 API/服务)nbf(若存在,确保不提前使用)
另外添加实用的防护:
- 强制最大令牌大小
- 拒绝意外的声明类型
- 允许小幅时钟偏差以应对时间漂移问题