什么是智威汤逊?通过工作示例解释 JSON Web 令牌

  1. 首页
  2. 博客
  3. 软件开发

软件开发

什么是智威汤逊? 通过工作示例解释 JSON Web 令牌

JWT 让服务器信任请求而无需查找任何内容。从头开始构建一个,尝试篡改它并观察它失败,然后了解令牌何时击败会话以及如何安全地使用它们。

阅读约 9 分钟
什么是智威汤逊?通过工作示例解释 JSON Web 令牌

要点速览

  • JWT 是一种紧凑的签名令牌,由三部分组成:标头、有效负载和签名。
  • 签名证明令牌没有被更改,因此服务器无需查找即可信任它。
  • 有效负载任何人都可以读取,因此切勿在其中放入秘密。
  • 令牌很难撤销,因此请保持它们的短暂性,并使用受信任的库对其进行验证。

您登录后,服务器如何知道后面的每个请求确实来自您?一种流行的答案是 JSON Web Token,或 JWT,通常发音为“jot”。它是客户端随每个请求发送的一小段经过签名的数据,服务器可以在不接触数据库的情况下检查它。

理解 JWT 的最快方法是构建一个 JWT,然后尝试破坏它。我们仅使用 Python 3.13 上的 Python 标准库来完成此操作,下面的输出来自该运行。 此代码供学习使用。在实际应用中,请使用维护良好的库,因为验证的细节很容易出错。

什么是 JWT?

JWT 是一个开放标准,在 RFC 7519 中定义,用于在两方之间以紧凑字符串的形式传递声明。声明只是一个声明,例如“该用户的 ID 是 42”。由于令牌已签名,因此接收者可以确认是谁颁发的并且没有人更改过它。

想想节日腕带。门口的工作人员检查了你的票一次,然后给你一个很难伪造的乐队。之后,每个摊位只看乐队,无需打电话给售票处。 JWT 的工作方式与 API 的工作方式大致相同。

JWT 登录的工作原理

  1. 用户将用户名和密码发送到登录端点。
  2. 服务器检查它们并创建签名令牌。
  3. 客户端存储令牌。
  4. 后面的每个请求都包含它,通常在标头中:Authorization: Bearer <token>。
  5. 服务器验证签名和有效期,然后读取声明。

主要好处是在步骤 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 和服务。

继续学习

与Stack Quora合作

需要测试、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%覆盖率仍然会错过错误,以及合理的目标。

阅读文章 →
数据库与 SQL

SQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们

DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。

阅读文章 →
数据库与 SQL

SQL 视图解释:创建、使用和避免陷阱(附示例)

SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。

阅读文章 →
软件开发

TypeScript 接口与类型:真正的差异,使用编译器进行测试

TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。

阅读文章 →
软件开发

Python *args 和 **kwargs 解释:灵活的函数和清晰的示例

Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。

阅读文章 →
软件开发

Python Lambda 函数:语法、实际用途以及何时不使用它们

Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。

阅读文章 →
人工智能

什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环

什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。

阅读文章 →
DevOps

监控与可观察性:真正的区别,包括日志、指标和跟踪

监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。

阅读文章 →
DevOps

Docker Volumes 与 Bind Mounts:以正确的方式保留容器数据

Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。

阅读文章 →
测试自动化

TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式

TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。

阅读文章 →
软件测试

用户验收测试 (UAT):流程、清单和真实示例

用户验收测试解释:谁运行它、它与 QA 有何不同、七步流程、UAT 测试用例模板、清单、签核标准和常见错误。

阅读文章 →
数据库与 SQL

SQL NULL 值解释:IS NULL、COALESCE、NULLIF 和要避免的陷阱

通过我们运行的查询解释了 SQL NULL 值:三值逻辑、IS NULL、COALESCE、NULLIF,聚合、连接和排序如何处理 NULL,以及它悄悄引起的错误。

阅读文章 →
数据库与 SQL

SQL 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 个示例以及如何计划测试。

阅读文章 →
软件测试

测试用例与测试场景:主要区别、示例以及何时使用它们

测试用例与测试场景的解释:清晰的定义、并排的表格、从场景到案例的密码重置功能、模板以及何时使用每个功能。

阅读文章 →
数据库与 SQL

SQL 查询优化:读取执行计划并修复慢速查询(带计时)

对一百万行进行 SQL 查询优化测试:读取执行计划、添加正确的索引、修复不可控制的过滤器、使用复合索引和键集分页。

阅读文章 →
数据库与 SQL

SQL 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 比较:架构、等待、浏览器和语言支持、调试、两种工具中的相同测试、迁移建议以及如何选择。

阅读文章 →
软件测试

等价划分:通过实例解释测试设计技术

等效分区解释:如何找到有效和无效分区,年龄、密码和优惠券的工作示例,我们运行的测试,以及为什么将其与边界配对。

阅读文章 →
软件测试

软件测试中的严重性与优先级:差异、矩阵和实例

软件测试中的严重性与优先级解释:清晰的定义、带有示例的四向矩阵、级别尺度、谁设置每个级别以及如何运行错误分类。

阅读文章 →
数据库与 SQL

SQL 子查询教程:类型、相关子查询和 EXISTS 以及示例

SQL 子查询教程,其中包含我们运行的查询:标量、IN、FROM 和 SELECT 子查询、相关子查询、EXISTS、NOT IN NULL 陷阱以及子查询与 JOIN 与 CTE。

阅读文章 →
数据库与 SQL

SQL 事务和 ACID 解释:COMMIT、ROLLBACK 和隔离级别

SQL 事务和 ACID 通过我们运行的银行转账进行了解释:COMMIT、ROLLBACK、保存点、锁、隔离级别、脏读、死锁和最佳实践。

阅读文章 →
软件开发

Git merge 与 rebase 的区别:示例及如何选择

用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。

阅读文章 →
软件开发

Python 列表推导式:语法、示例与何时避免使用

Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。

阅读文章 →
数据库与 SQL

SQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)

通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。

阅读文章 →