- JWT 是一种紧凑的签名令牌,由三部分组成:标头、有效负载和签名。
- 签名证明令牌没有被更改,因此服务器无需查找即可信任它。
- 有效负载任何人都可以读取,因此切勿在其中放入秘密。
- 令牌很难撤销,因此请保持它们的短暂性,并使用受信任的库对其进行验证。
您登录后,服务器如何知道后面的每个请求确实来自您?一种流行的答案是 JSON Web Token,或 JWT,通常发音为“jot”。它是客户端随每个请求发送的一小段经过签名的数据,服务器可以在不接触数据库的情况下检查它。
理解 JWT 的最快方法是构建一个 JWT,然后尝试破坏它。我们仅使用 Python 3.13 上的 Python 标准库来完成此操作,下面的输出来自该运行。 此代码供学习使用。在实际应用中,请使用维护良好的库,因为验证的细节很容易出错。
什么是 JWT?
JWT 是一个开放标准,在 RFC 7519 中定义,用于在两方之间以紧凑字符串的形式传递声明。声明只是一个声明,例如“该用户的 ID 是 42”。由于令牌已签名,因此接收者可以确认是谁颁发的并且没有人更改过它。
想想节日腕带。门口的工作人员检查了你的票一次,然后给你一个很难伪造的乐队。之后,每个摊位只看乐队,无需打电话给售票处。 JWT 的工作方式与 API 的工作方式大致相同。
JWT 登录的工作原理
- 用户将用户名和密码发送到登录端点。
- 服务器检查它们并创建签名令牌。
- 客户端存储令牌。
- 后面的每个请求都包含它,通常在标头中:
Authorization: Bearer <token>。 - 服务器验证签名和有效期,然后读取声明。
主要好处是在步骤 5 中。服务器不必查找会话,这在许多服务或服务器需要检查相同令牌时很方便。如果您不熟悉 API,请从什么是 API 开始。
JWT 的三个部分
JWT 看起来像随机文本,但它是由点分隔的三个 Base64URL 编码部分。这是我们生成的:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiI0MiIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzkzNTAwMDAwLCJleHAiOjE3OTM1MDA5MDB9
.7YfUDH_T-iDh8bMmUT8mmKYwOstzT8Ty--DTTAmQGWM
我们添加了换行符以使其更易于阅读。解码前两部分给出纯 JSON:
Header: {"alg":"HS256","typ":"JWT"}
Payload: {"sub":"42","role":"user","iat":1793500000,"exp":1793500900}
| 部分 | 包含 | 目的 |
|---|---|---|
| 标头 | 令牌类型和签名算法 | 说明如何验证令牌 |
| 有效载荷 | 关于用户或会话的声明 | 实际信息 |
| 签名 | 前两部分的哈希值,使用密钥 | 证明完整性和来源 |
标准声明
| 索赔 | 名称 | 含义 |
|---|---|---|
子 |
主题 | 令牌是关于谁的,通常是用户 ID |
国际空间站 |
发行人 | 谁创建了代币 |
音频 |
观众 | 令牌的用途 |
经验值 |
到期日 | 当令牌不再有效时 |
亚特 |
发布于 | 当它被创建时 |
NBF |
之前没有 | 当它开始有效时 |
吉蒂 |
智威汤逊ID | 该令牌的唯一 ID |
您还可以添加自定义声明,例如 role。时间是 Unix 时间戳,以秒为单位。保持较小的有效负载,因为令牌会随每个请求一起传输。
自己建一个
创建令牌涉及对两个 JSON 部分进行编码,然后使用 HMAC-SHA256 使用密钥对它们进行签名:
import base64, hashlib, hmac, json
SECRET = b"demo-secret-do-not-use-in-production"
def b64url(data):
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
def sign(message):
return b64url(hmac.new(SECRET, message.encode(), hashlib.sha256).digest())
def create_token(payload):
header = {"alg": "HS256", "typ": "JWT"}
parts = [
b64url(json.dumps(header, separators=(",", ":")).encode()),
b64url(json.dumps(payload, separators=(",", ":")).encode()),
]
message = ".".join(parts)
return message + "." + sign(message)
验证通过重新计算签名并将其与令牌中的签名进行比较来进行。只有知道秘密的人才能产生匹配的秘密:
def verify_token(token, now):
header, payload, signature = token.split(".")
expected = sign(header + "." + payload)
if not hmac.compare_digest(signature, expected):
raise ValueError("bad signature")
claims = json.loads(b64url_decode(payload))
if claims["exp"] < now:
raise ValueError("token expired")
return claims
有效令牌按预期返回其声明。使用compare_digest是因为它以恒定时间运行,这避免了由于时间差异而泄漏信息。
尝试篡改它
假设攻击者解码有效负载,将 "role":"user" 更改为 "role":"admin",再次对其进行编码并保留旧签名。我们尝试了这个以及其他两种攻击:
| 尝试 | 结果 |
|---|---|
| 将角色更改为管理员 | 不好的签名 |
| 过期后 1 秒使用令牌 | 令牌已过期 |
设置"alg":"none"并删除签名 |
不好的签名 |
最后一次尝试针对的是一个真正的历史弱点。一些库曾经信任令牌自己的标头,因此攻击者可以简单地声明不需要签名。我们的代码总是使用一种固定的算法,这就是它被拒绝的原因。在真实的库中,始终指定您接受的算法。
已签名,未加密
这是关于 JWT 最容易被误解的一点。我们的伪造令牌被拒绝,但其内容仍然完全可读:
{"sub":"42","role":"admin","iat":1793500000,"exp":1793500900}
Base64URL 是一种编码,而不是加密,任何人都可以在没有密钥的情况下逆向它。签名可以阻止人们更改数据,但不能阻止读取数据。因此,切勿将密码、卡号或私人个人数据放入 JWT 有效负载中。如果必须隐藏内容,请使用加密令牌 (JWE),或者仅将数据保留在服务器上。
HS256 与 RS256
| 方面 | HS256 | RS256 或 ES256 |
|---|---|---|
| 类型 | 对称 | 不对称 |
| 按键 | 一个共同的秘密 | 私钥签名,公钥验证 |
| 谁可以创建代币 | 任何掌握秘密的人 | 仅私钥持有者 |
| 最适合 | 一项服务或一个值得信赖的小团体 | 许多服务、第三方、身份提供商 |
通过非对称签名,其他服务可以在无法铸造自己的令牌的情况下验证令牌,这限制了一旦受到威胁所造成的损失。
JWT 与会话
JWT 通常被视为会话的现代替代品。实际上,每种方法都需要权衡:
| 方面 | 服务器会话 | 智威汤逊 |
|---|---|---|
| 国家居住的地方 | 在服务器上 | 在代币中 |
| 针对每个请求进行查找 | 通常 | 不需要 |
| 注销或撤销 | 简单,删除会话 | 到期前很难 |
| 按要求尺寸 | 小ID | 更大的令牌(我们的是 161 个字符) |
| 跨越许多服务 | 需要共享存储 | 每个都可以直接验证 |
| 更改权限 | 立即适用 | 旧令牌在到期前保持有效 |
对于传统的服务器渲染网站,会话 cookie 通常更简单、更安全。 JWT 对于多个服务或移动客户端使用的 API 往往更有意义。
提示:不要仅仅因为 JWT 听起来无状态就选择它。许多实际系统添加拒绝列表或数据库检查来支持注销,这会带回 JWT 本来要避免的查找。
在浏览器中存储令牌的位置
| 地点 | XSS 风险 | 跨站请求伪造风险 | 注释 |
|---|---|---|---|
本地存储 |
高,因为脚本可以读取它 | 低 | 简单,但会暴露于任何注入的脚本 |
| 仅 Http cookie | 较低,因为脚本无法读取它 | 需要保护 | 使用Secure和SameSite |
| 内存变量 | 较低 | 低 | 刷新时丢失,因此需要刷新流程 |
许多安全指南都支持 HttpOnly、Secure、SameSite Web 应用程序的 cookie。无论您选择哪种,请始终使用 HTTPS。要从 JavaScript 发送令牌,请参阅我们的 JavaScript Fetch API 教程。
刷新令牌
短暂的有效期限制了令牌被盗造成的损害,但没有人愿意每 15 分钟登录一次。通常的解决方案使用两个令牌:
- 访问令牌: 短暂的,例如几分钟,并随每个 API 请求一起发送。
- 刷新令牌:寿命更长,存储更仔细,仅用于获取新的访问令牌。
服务器会跟踪刷新令牌,因此可以撤销它们,从而为您提供实用的注销。每次使用时轮换它们还可以帮助您检测盗窃行为,因为被盗的旧令牌会被重放。
常见的安全错误
- 解码而不验证。读取有效负载并不使其值得信赖。始终检查签名。
- 接受任何算法。设置允许的算法的明确列表。
- 使用弱秘密。短秘密可以离线破解。使用长随机值,并使其不受源代码控制。
- 跳过过期。没有
exp的令牌如果被盗可以永远有效。 - 不检查
iss和aud。否则为一项服务颁发的令牌可能会被另一项服务接受。 - 信任过时的声明。令牌内的角色可能会过时,直到过期。
- 记录令牌。任何阅读您日志的人都可以重复使用它们。
- 不测试失败路径。 添加对过期和修改令牌以及有效令牌的测试。我们的使用 Postman 进行 API 测试 指南可以提供帮助。
最后的话
JWT 是服务器无需查找即可信任的签名数据,不是秘密容器,也不是会话自动升级。使用较短的有效期,使用维护的库验证所有内容,并将私有数据保留在有效负载之外。要亲自查看,请运行一个像我们这样的小脚本,更改令牌的一个字符并观察验证失败。
常见问题
JWT 有何用途?
JWT 主要用于 API 中的身份验证和授权。登录后,客户端在每个请求中发送令牌,服务器验证它以识别用户。
JWT 是否加密?
通常不会。标准 JWT 经过签名和编码,因此任何人都可以读取其有效负载。签名仅防止更改。加密令牌使用称为 JWE 的相关格式。
JWT 应该持续多长时间?
访问令牌通常是短暂的,通常是几分钟而不是几天。将它们与刷新令牌配对,这样用户就不必经常登录。
JWT 可以撤销吗?
不是单独的,因为它在过期之前一直有效。撤销需要服务器端跟踪,例如拒绝名单,这会消除一些无状态的好处。
JWT 比会话更好吗?
两者都不总是更好。会话更容易撤销并适合许多网站,而 JWT 适合需要独立验证身份的 API 和服务。
继续学习
需要测试、WordPress 或 SEO 方面的帮助?
我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。
更多指南 测试、人工智能和现代开发
喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。
人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它
人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。
阅读文章 →DevOps环境变量解释:.env 文件、秘密和配置正确完成
使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。
阅读文章 →软件开发API 速率限制解释:算法、标头和处理 429 错误
API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。
阅读文章 →测试自动化Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误
使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。
阅读文章 →软件测试代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义
用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。
阅读文章 →数据库与 SQLSQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们
DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。
阅读文章 →数据库与 SQLSQL 视图解释:创建、使用和避免陷阱(附示例)
SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。
阅读文章 →软件开发TypeScript 接口与类型:真正的差异,使用编译器进行测试
TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。
阅读文章 →软件开发Python *args 和 **kwargs 解释:灵活的函数和清晰的示例
Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。
阅读文章 →软件开发Python Lambda 函数:语法、实际用途以及何时不使用它们
Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。
阅读文章 →人工智能什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环
什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。
阅读文章 →DevOps监控与可观察性:真正的区别,包括日志、指标和跟踪
监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。
阅读文章 →DevOpsDocker Volumes 与 Bind Mounts:以正确的方式保留容器数据
Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。
阅读文章 →测试自动化TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式
TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。
阅读文章 →软件测试用户验收测试 (UAT):流程、清单和真实示例
用户验收测试解释:谁运行它、它与 QA 有何不同、七步流程、UAT 测试用例模板、清单、签核标准和常见错误。
阅读文章 →数据库与 SQLSQL NULL 值解释:IS NULL、COALESCE、NULLIF 和要避免的陷阱
通过我们运行的查询解释了 SQL NULL 值:三值逻辑、IS NULL、COALESCE、NULLIF,聚合、连接和排序如何处理 NULL,以及它悄悄引起的错误。
阅读文章 →数据库与 SQLSQL CASE WHEN 语句:语法、实例和常见错误
SQL CASE WHEN 解释了我们运行的查询:简单和搜索表单、条件顺序、SUM 透视、COUNT 技巧、自定义排序、更新和要避免的 NULL 陷阱。
阅读文章 →软件开发什么是智威汤逊?通过工作示例解释 JSON Web 令牌
什么是智威汤逊? JSON Web Tokens 解释:这三个部分,在我们运行的代码中构建和篡改的令牌,JWT 与会话,存储,刷新令牌和安全错误。
阅读文章 →软件开发JavaScript 事件循环解释:调用堆栈、微任务和计时器以及示例
JavaScript 事件循环用我们运行的代码进行了解释:调用堆栈、任务和微任务队列、为什么 setTimeout 0 不是立即执行、阻塞、饥饿和面试测验。
阅读文章 →软件开发Python 生成器和 Yield 解释:通过真实示例节省内存
Python 生成器和yield 用我们运行的代码进行了解释:它们如何暂停和恢复、测量内存节省、管道、无限序列、yield from、发送和常见陷阱。
阅读文章 →人工智能什么是嵌入?向量和余弦相似度简单解释
什么是嵌入?关于向量、余弦相似度和语义搜索的简单语言指南,其中包含我们运行的一个小示例、常见用途、选择模型和已知限制。
阅读文章 →DevOps蓝绿部署与金丝雀部署:差异、权衡以及如何选择
蓝绿部署与金丝雀部署解释:每种部署的工作原理、成本、风险和回滚、我们测试的稳定用户分桶、数据库迁移、滚动更新以及如何选择。
阅读文章 →测试自动化不稳定的测试:造成这些问题的原因以及如何彻底解决它们
我们运行的演示解释了不稳定的测试:主要原因、如何确认和诊断、正确的时间修复、共享状态和日期以及隔离过程。
阅读文章 →软件测试负载测试与压力测试:差异、指标和 k6 示例
负载测试与压力测试的解释:各自证明什么、峰值和浸泡测试、重要的指标、为什么平均值会误导、k6 个示例以及如何计划测试。
阅读文章 →软件测试测试用例与测试场景:主要区别、示例以及何时使用它们
测试用例与测试场景的解释:清晰的定义、并排的表格、从场景到案例的密码重置功能、模板以及何时使用每个功能。
阅读文章 →数据库与 SQLSQL 查询优化:读取执行计划并修复慢速查询(带计时)
对一百万行进行 SQL 查询优化测试:读取执行计划、添加正确的索引、修复不可控制的过滤器、使用复合索引和键集分页。
阅读文章 →数据库与 SQLSQL UNION 与 UNION ALL:差异、示例和性能测试
SQL UNION 与 UNION ALL 通过我们运行的查询进行了解释:重复、都遵循的规则、真实的数据错误、测量的性能、INTERSECT 和 EXCEPT 以及何时使用它们。
阅读文章 →软件开发JavaScript Fetch API 教程:GET、POST、错误处理和超时
针对本地服务器测试的 JavaScript Fetch API 教程:GET 和 POST、为什么不抛出 404、可重用的帮助程序、查询字符串、超时、CORS 和常见错误。
阅读文章 →软件开发Python 文件处理:读取、写入、CSV、JSON 和 pathlib 以及示例
使用我们运行的代码解释了 Python 文件处理:打开模式、使用、读取大文件、CSV、JSON、pathlib、二进制文件、安全写入、错误处理和常见错误。
阅读文章 →软件开发Python 类和对象:带有实际示例的 OOP 教程
用我们运行的代码解释 Python 类和对象:__init__、方法、类与实例属性、继承、属性、dunder 方法、数据类和常见错误。
阅读文章 →人工智能什么是大语言模型?法学硕士如何运作,在没有炒作的情况下进行解释
什么是大语言模型?法学硕士用简单的语言解释:标记、嵌入、注意力、训练、温度和上下文窗口、一个微小的下一个单词演示和真正的限制。
阅读文章 →DevOps适合初学者的 Linux 命令:带有示例的基本终端指南
适合初学者的 Linux 命令,按任务分组:导航、文件、查看日志、grep、管道、权限、进程和档案,并附有实用的日志分析示例。
阅读文章 →测试自动化Selenium 与 Playwright:主要区别、代码比较以及选择
Selenium 与 Playwright 比较:架构、等待、浏览器和语言支持、调试、两种工具中的相同测试、迁移建议以及如何选择。
阅读文章 →软件测试等价划分:通过实例解释测试设计技术
等效分区解释:如何找到有效和无效分区,年龄、密码和优惠券的工作示例,我们运行的测试,以及为什么将其与边界配对。
阅读文章 →软件测试软件测试中的严重性与优先级:差异、矩阵和实例
软件测试中的严重性与优先级解释:清晰的定义、带有示例的四向矩阵、级别尺度、谁设置每个级别以及如何运行错误分类。
阅读文章 →数据库与 SQLSQL 子查询教程:类型、相关子查询和 EXISTS 以及示例
SQL 子查询教程,其中包含我们运行的查询:标量、IN、FROM 和 SELECT 子查询、相关子查询、EXISTS、NOT IN NULL 陷阱以及子查询与 JOIN 与 CTE。
阅读文章 →数据库与 SQLSQL 事务和 ACID 解释:COMMIT、ROLLBACK 和隔离级别
SQL 事务和 ACID 通过我们运行的银行转账进行了解释:COMMIT、ROLLBACK、保存点、锁、隔离级别、脏读、死锁和最佳实践。
阅读文章 →软件开发Git merge 与 rebase 的区别:示例及如何选择
用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。
阅读文章 →软件开发Python 列表推导式:语法、示例与何时避免使用
Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。
阅读文章 →数据库与 SQLSQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)
通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。
阅读文章 →