Laravel项目中,我的一个项目上的一个许可证激活端点开始拒绝它所交出的每一个令牌,不是某些令牌,每一个令牌,包括同一服务器早九十秒发出的日志说 token expired、代币没有过期,我花了一个星期六的大部分时间都确信盒子上的时钟已经飘了。
它有't。错误是一行:
if (payload.exp < Date.now()) throw new Error('token expired')
exp 在 JWT 中是 秒 由于时代 - RFC 7519 §4.1。4 对此毫不含糊。 Date.now() 在 JavaScript 中返回 毫秒.所以我在用一个十位数和十三位数比较,十位数总是小一些,宇宙中的每一个令牌都过期了,永远,修复的是 Date.now() / 1000。诊断花了八个小时,因为我从来没有真正做过 看了 令牌上 - 我不断重新阅读自己的代码,这相当于在戴眼镜时搜索眼镜的调试。
最终打破循环的是将令牌粘贴到解码器中,读取 exp: 1748952000、将其转换为日期,并在未来两周内看到时间戳,令牌没问题,我的比较是错误的,看数据三十秒超过看代码八小时。
That's 本指南是关于什么的 不聪明的调试哲学 - 我在一个名为"API Debug,"的选项卡组中保留的具体,无聊,不迷人的浏览器工具,每个实际上都是为了什么,以及他们捕获的失败模式 这里的一切都在客户端运行 Toolz.dev的,这比听起来更重要,就像当你're即将粘贴的东西是生产承载令牌时。
TL;博士: API 行为不端时,停止读取您的代码并开始读取有效负载。将响应格式化为 json格式化程序。用破解令牌 jwt解码器不是通用的 Base64 工具。转动
exp、iat、和X-RateLimit-Reset与真实约会 时间戳转换器。将工作响应与损坏响应进行比较 文本差异 或者,更好的是 JSON 迪夫。解码查询-字符串破坏与 URL 编码器。所有内容都在您的浏览器中运行 - 令牌永远不会越过电线。
为什么调试 API 的感觉比调试代码差得多?
因为你可以't 跨过它 本地bug 有堆栈跟踪 有调试器 有断点 API bug 有字符串 其他人's 服务器生产了那个字符串,按照你只知道一半的规则,你的工作就是从它向后工作。
那倒置了一般的技能瓶颈是't逻辑,它's 易读性。几乎每一个API bug I've在过去几年中追逐的都是隐形的,直到我让数据可读:
- 4000个字符的简化的JSON响应,结果证明是
"data": null埋在六号深处。 - A Base64 有效负载,它解码为错误消息 API 太礼貌了,无法放入状态代码。
- 一个由于主体有一个尾随换行符而导致签名验证失败的网络挂钩,我的 HTTP 客户端添加了一个有益的换行符。
- 当文档说秒时,时间戳以毫秒为单位。 (两次。不同的公司。)
这些都不是难题。他们都是 无法读取 problems。fellow的工具的存在,使数据足够快地清晰可辨,你注意到你的眼睛会滑过的东西。
哪种症状的工具?
这是我希望五年前有人递给我的桌子。左边是症状,先向右移动。
| 的症状 | What's通常是真的 | 第一步 |
|---|---|---|
| Response是一条巨型线,可以't看到结构 | 没有什么's破碎,它's刚刚最小化 | json格式化程序 |
401/403 在你刚刚铸造的代币上 |
时钟、声明或比较错误 | jwt解码器 → 检查 exp、 aud、 iss |
| 日期显示为 1970 年或 56122 年 | 秒/毫秒不匹配 | 时间戳转换器 |
| "昨天它起作用了" | 一个领域改变了形状 | JSON 迪夫 新旧响应 |
| 帕拉姆到达时被损坏或截断 | 双编码,或者未逃脱的 &/+ |
URL 编码器 |
| 网络钩签名永远不匹配 | Body字节与您're散列的不同 | 哈希发生器 在确切的原始身体上 |
Authorization: Basic ... 拒绝 |
证书编码错误,或存在误差 | Base64 转换器 |
| Config 驱动的部署失败,API 甚至从未运行过 | YAML 缩进 | YAML 验证器 |
| 两个反应看起来相同,但行为不同 | 隐形人物 | 文本差异 |
下面的一切都是该表的长版本。
如何使 API 响应在十秒内可读?
粘贴到 json格式化程序。那's整个技术,而I'm不油嘴滑舌- 我拥有的单一最高杠杆调试习惯是拒绝 的理由 我已设置的有效负载't 格式化。
Here's我从计费提供商那里得到的响应形状,就像它从电线上脱落一样:
{"subscriptions":[{"id":"sub_7f3d8a2b","status":"active","plan":{"id":"pro_annual","interval":"year","amount":9900},"current_period_end":1748952000,"cancel_at_period_end":false}],"has_more":false}
Formated,它's完全不同的对象-不是对解析器,而是对我:
{
"subscriptions": [
{
"id": "sub_7f3d8a2b",
"status": "active",
"plan": {
"id": "pro_annual",
"interval": "year",
"amount": 9900
},
"current_period_end": 1748952000,
"cancel_at_period_end": false
}
],
"has_more": false
}
现在我可以看到这一点 amount 於 9900 而没有 99.00- it's以美分计,这是支付中最常见的集成错误 - 并且 current_period_end 是十位整数,表示秒,表示don't交给 new Date() 直接。
验抓住了你眼睛赢的't
Formatting 也验证了,解析失败就是信息,实际有效负载中实际出现的错误:
- 尾部逗号。 JavaScript 合法,JSON 非法(per RFC 8259)。手工编辑的夹具里装满了。
- 单引号。 JSON需要双引号。Python's
str(dict)输出不是 JSON,无论它看起来有多像。 - 未引用的键。 同样的故事 - that's 是 JavaScript 对象的字面意思,而不是 JSON。
NaN/Infinity。 有些序列化器会发射它们。 JSON 没有这样的字面意思。- 一颗炸弹。 前面有一个 UTF-8 字节顺序标记
{将使严格的解析器拒绝屏幕上看起来完美的文档。
如果您的 JSON 有效,但 形状 是错误的,那's一个不同的工具 - 请参阅下面的差异部分。
为什么使用 JWT 解码器而不是仅使用 Base64 解码令牌?
Base64-手解码一个JWT,我做了好多年,它's一个坏习惯,而这里's为什么。
JWT(RFC 7519)是三个由点分隔的块:头、有效负载、签名,每个块是 base64url的,而不是标准的base64 - RFC 4648 §5,互换的 URL 安全字母表 +→- 並 /→_ 通常会掉落 = padding。feed到一个严格的标准-base64解码器,它要么错误,要么默默地给你垃圾字节。所以手法就是在点上分裂,重新填充,交换字母表,每次,然后眼球生json。
的 jwt解码器 does所有这些在一个粘贴和 - 实际上节省时间的部分 - 将索赔作为索赔呈现。我检查的, 顺序:
exp(到期)和iat(发行于)- 两者 数字日期即 秒 1970-01-01 UTC以来。这是吃我星期六的场地。aud(观众) - 为您的暂存 API 铸造的代币在结构上将是完美的,但仍被 prod 拒绝。iss(发行人)- 在身份提供者迁移之后,这是悄然改变的领域。alg在标头中 - 如果它说none的,你有安全问题,而不是调试问题。
以规范示例令牌每个人's看到:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
标头: {"alg":"HS256","typ":"JWT"}。有效载荷: {"sub":"1234567890","name":"John Doe","iat":1516239022}. 和 iat 有 2018-01-18T01:30:22Z- 您只能通过时间戳转换器运行它才能知道这一点,这是下一节的全部内容。
解码器不做的事情
Decoding不是验证,任何人都可以解码一个JWT;有效载荷是编码的,不是加密的,一个解码器告诉你什么令牌 声称、永远不要声称是否属实签名验证发生在你的服务器上你的秘密,任何浏览器工具都不应被交给那个秘密。像对待你'd那样对待解码的JWT 将表单提交:视为陌生人的断言。
如何停止错误获取时间戳?
学读数位数,这是整个领域最便宜的调试技巧,需要一分钟时间学习。
| 数字 | 单位 | 例子 | new Date(x) JS中给你 |
|---|---|---|---|
| 10 | 秒 | 1748952000 |
1970-01-21 - 显然是错误的 |
| 13绔 涔诲芥澶澧 | 毫秒 | 1748952000000 |
的正确日期 |
| 16绔 涔诲芥澶澧 | 微秒 | 1748952000000000 |
胡说 |
十位数字表示秒 十三表示毫秒 JavaScript's Date 构造函数想要毫秒; Unix、Python's time.time(),去's Unix(),PHP's time()的,以及大多数API''',2019年10月20日,中国国家统计局关于开展全国性疫情防控工作的通知 exp fields 说秒。那个不匹配的下游一切都是混乱的。
而乱象在一个方向上大声,在另一个方向上沉默。喂 秒 到毫秒解析器,你就会得到1970- 一个如此明显的错误,你可以在一分钟内修复它。喂 毫秒 到a 秒 解析器,你就得到了年份 第56122章。我查了一下: 1708876200000解释为秒,于 56122 年 2 月 17 日登陆。that's 悄悄运送的方向,因为没有任何东西会抛出 - 订阅永远不会过期,四分之一的人不会注意到。
的 时间戳转换器 exist 所以你可以把这个放在一个粘贴里,而不是争论它粘贴 1748952000、读日期,继续。粘贴 X-RateLimit-Reset header你的api在抱怨,发现你有四分钟的等待时间,而不是四个小时如果时间戳是天真的(不 Z、无偏移),并且您需要推理它在另一个地区意味着什么 时区转换器 是后续。
还有两个值得了解的时间戳陷阱
2038年的问题是真实的,而且已经过时了。 有符号的 32 位秒计数器溢出 2147483647,即 2038-01-19T03:14:07Z。任何系统仍然将时间存储在有符号的32位int中 - 并且嵌入式和遗留数据库列中的时间比任何人都想承认的要多 - 那么就中断。如果您're设置长寿命到期今天,您已经能够击中此项。
天真的时间戳是不作为的谎言。 2026-02-25T14:30:00 没有落后 Z 和没有 +05:30 is not a moment in time; it's 在未指定地点的时刻,我把任何返回幼稚时间戳的 API 当成等待归档的错误报告,比较喜欢 RFC 3339 (2026-02-25T14:30:00Z的),这是网络实际使用的ISO 8601的严格、明确的配置文件。
当 "昨天有效时我该怎么办"?
Diff it。Don't 理论化 - 差异化它。
(或从日志中挖出最后一个好的)工作环境的响应和坏掉的响应, 并排放在一起, 十分之九正好差一个它's盯着你五秒钟内。
对于 JSON,触及 JSON 迪夫 diff 文本之前。它解析两面并进行比较 结构的,这意味着重新排序的键和不同的缩进don't显示为更改 - 只有真实的才有。两个JSON文档的文本差异,服务器以不同的键顺序序列化将像圣诞树一样点亮,并且不会告诉您任何信息。
对于所有内容't JSON - 标头、原始主体、配置文件、 curl 输出 - 使用 文本差异。它的专长是改变你的眼睛在身体上无法捕捉的类别:一个尾随空间,一个选项卡变成了四个空间,一个CRLF线结束从Windows机器偷偷溜进来,一个卷曲的引用,一个文档网站代替了直的当你复制例子时,I've写了一整块 为什么眼球文本比较失败的,因为这让我失去了一次支持升级。
为什么我的查询参数不断损坏?
因为 URL 编码有三到四种微妙不同的味道,每个人's 堆栈都会选择不同的味道。
的经典,按照他们've咬我的频率的粗略顺序:
+对比%20。 在查询字符串中,+历史上是指一个空间(theapplication/x-www-form-urlencoded约(约定)。在路径段中,+意思是字面加号。因此 base64 签名包含+、掉进一个未编码的查询字符串,到时里面有空格,签名检查失败,这是一个真正令人讨厌的,因为值 看起来 就在日志中。- 双编码。
%2F变成%252F因为你的堆栈的两层都有助于编码它。症状是一个每次通过代理时都会获得百分比符号的参数。 - 一个原始的
&在一个值内。 将参数分成两部分。现在name=Ben & Jerry於name=Ben再加上一个神秘的帕拉姆,叫做Jerry。
将 URL 粘贴到 URL 编码器 并解码它。如果解码一次仍然留下百分之百逃逸,你've找到了你的双重编码。那's整个诊断。
如何调试获胜的 Webhook't 验证?
这是将"我理解HTTP"与"我'一直待命。"
Webhook 提供商几乎都用 HMAC 签署有效负载,并将结果放入头,你的工作就是计算相同的 HMAC 并进行比较,当它不't 匹配时,签名几乎从来都不是问题。 字节就是问题所在。 你不会散列他们散列的东西。
通常的嫌疑人:
- 您对解析并重新序列化的正文进行了哈希处理。 您的框架将 JSON 解析为一个对象,您调用了该对象
JSON.stringify()on it,而现在键序或者空白相差一个字符,你必须哈希 the 原始 request body,接收到的字节。在 Express 中,这意味着捕获之前的原始缓冲区express.json()达到它;在拉拉维尔语中是这样的意思$request->getContent()不是$request->all()。 - 尾随的换行符。 1些客户追加1。提供商没有't。
- You're散列十六进制编码的字符串,而不是原始字节的,或者将六角形与底数进行比较64。
- 查塞特。 身体有一个多字节字符,并且一路上有一些东西对其进行转码。
的 哈希发生器 我是如何隔离这个的:获取精确的正文字符串,哈希它,与我的代码为它生成的代码进行比较 思想 was同一个body。if that two hashes different, my code is not see the bytes I think it's seeing, and the problem was never cryptographical at all。(也值得知道:如果提供商仍然提供MD5或SHA-1签名,that's是一个关于他们平台年龄的信号。SHA-256现在是地板。)
调试选项卡组中还存在哪些内容?
配角阵容--不那么迷人,仍然赢得了自己的位置:
- 用户识别器- 为干净
X-Request-IDon every test call, 所以你可以跨三个服务 grep ' logs 之后。值得知道的是 UUID 得到了一个规范刷新: RFC 9562 (2024) 已过时 RFC 4122 和标准化 UUIDv7的,这是时间排序的,因此对你的数据库's B树索引比随机v4要友好得多。如果你're今天为一个新表选择一个ID方案,那's上读到的。那里's a UUID版本的更长细分 如果你想要的话。 - Base64 转换器- 为
Authorization: Basic头(RFC 7617:it'sbase64(user:password),并且是的,that's编码,而不是安全性- TLS是保护它的),并且对于内联二进制blobs一些API的东西到JSON字段中,Base64花费你 ~ 33%大小开销,这就是为什么"意想不到的大"有效负载通常只是其中有一个文件 全 Base64 指南 涵盖了引导人们的 base64url 区别。 - YAML 验证器-因为我去年一半的 API 失败是 't API 失败。它们是 CI 配置中的两个空间缩进错误,并且端点根本没有部署。 (YAML 的失败模式比损坏的构建更糟糕,不过:有效的 YAML 意味着您没有的东西't 打算。我写了 为什么
version: 1.10变为1.1 在我花费了部署之后。) - 正则表达式测试仪- 目前您需要从 900 行日志中提取一个请求 ID,并使用您'没有自信的模式。
- CSV 查看器- 对于出口端点,您需要在有人将其进口到生产之前对其输出进行理智检查。
这些在浏览器中运行实际上很重要吗?
是的,即使我没有't 建造了该网站,I'd 也会这么说。
想想你粘贴到调试工具中的内容。 JWT - 这是一个 实时凭证 until it expirates。a 制作 API 响应,即客户数据:姓名、电子邮件、订阅声明。webhook 体,其中可能包含付款记录。A curl 命令与 Authorization 标题仍在其中。
Now consider that a server-side tool, by definition, received all of that。not maliciously - just architecturally。paste进入一个HTTP请求, 击中某人's后端, 落在他们碰巧运行的任何日志记录中。即使是一个严格诚实的操作员最终也会将你的承载令牌放在他们从未打算保留的访问日志中。
toolz。dev上的工具在你的选项卡中用JavaScript完成工作。什么都没有上传,因为那里's无处上传到-解析,解码,散列都发生在你的机器上。你不't必须接受我的话,要么:打开DevTools,去网络选项卡,粘贴一个令牌,并观察一个永远不会来的请求。那个's三十秒的审核,你应该运行它 任何 工具你粘贴秘密,包括我的。我写了 如何正确验证客户端工具 正是出于这个原因。
如果您的组织处理欧盟个人数据,这是'不仅卫生 - 将客户记录粘贴到第三方服务器中是一项处理活动,所有 GDPR 文件都意味着。客户端工具根本不会成为处理器,从而回避了这个问题。
实际坚持的工作流程
6个步骤, 按照我运行它们的顺序当什么's着火:
- 捕获原始响应。 Full body,全头,状态码。不是你的app's对其的解释- 实际的字节。
curl -i或网络选项卡's"复制为cURL。" - 格式化它。 json格式化程序.看看 形状 在看值之前。您需要的字段是否存在?
- 解码每个不透明字符串。 代币通过 jwt解码器,Base64 斑点穿过 Base64 转换器、被破坏的网址通过 URL 编码器.不透明的字符串令人惊讶地经常隐藏答案。
- 将每个可能是一个时间的数字变成一个日期。 时间戳转换器.先数位数。
- 与已知的良好反应不同。 JSON 迪夫。如果你没有't有一个已知的-良好的反应,这是你开始拯救他们的标志。
- 现在才去读你的代码。 此时,您通常在打开文件之前就知道该行。
Direction很重要,第六步是我以前开始的地方,它's为什么那个星期六要花八个小时。
常见问题
API调试最好的免费工具有哪些?
API 的日常调试,您需要五件事:一个 JSON 格式化程序和验证器、一个 JWT 解码器、一个 Unix 时间戳转换器、一个 diff 工具和一个 URL 编码器/解码器。这五件事在 toolz。dev 上都是免费的,并且完全在浏览器中运行。如果您使用有签名的网络挂钩,则添加哈希生成器;如果您的部署是配置驱动的,则添加 YAML 验证器。
JWT 或 API 响应粘贴到在线工具中是否安全?
仅当工具是客户端时。JWT 是实时凭据,API 响应通常是客户数据,因此服务器端工具意味着将两者都发送给陌生人's 后端。toolz。dev 工具使用 JavaScript 处理浏览器中的所有内容,并且不会通过网络发送任何内容 - 通过打开 DevTools'网络选项卡自行验证这一点,同时粘贴。在您使用敏感数据的任何工具上运行相同的检查。
为什么我的 JWT 说 "过期"当我刚刚生成它时?
最常见的原因是单位不匹配。 exp 声明以秒为单位(RFC 7519 将其定义为 NumericDate),但 JavaScript's Date.now() return 毫秒,所以直接比较它们会让每个令牌看起来都过期了,解码令牌,读 exp(timestamp converster) 的时间戳转换器将其转换为真实日期,并在您触摸您的 auth 代码之前检查它是否实际上是过去的。
Unix时间戳是秒还是毫秒,我该怎么分辨?
数数字。十位数是秒,十三位数是毫秒,十六位数是微秒。如果 1970 年出现日期,您将秒输入到毫秒解析器;如果 56122 年出现日期,您将毫秒输入到秒解析器。第二个错误更危险,因为没有什么会带来错误。
我可以使用这些工具调试 GraphQL API 吗?
是的。 GraphQL 响应是 JSON,因此 JSON 格式化程序和 JSON diff 工作不变,并且 GraphQL 通常使用相同的承载令牌身份验证 you'd 使用 JWT 解码器进行解码。唯一真正的区别是 GraphQL 返回 HTTP 200,其中包含一个 errors array而不是非2xx状态,所以总是格式化主体 - 故障是在有效负载内部,而不是在状态代码中。
为什么我的 Webhook 签名验证总是失败?
几乎总是因为您正在散列与提供商不同的字节。如果您的框架解析了 JSON 体,并且在散列之前重新序列化了它,则空格或密钥顺序已更改,HMAC 将永远不会匹配。将原始请求体与收到的完全相同进行散列,并检查您的 HTTP 客户端添加的尾随换行符。
Base64和base64url编码有什么区别?
标准 Base64 (RFC 4648 §4) 使用 + 並 / 在其字母表和垫中 =。 base64url (RFC 4648 §5) 替换为 - 並 _ 并且通常会丢弃填充,因此将值放入 URL 或 JWT 是安全的。将 base64url 数据馈送到严格的标准 Base64 解码器会产生错误或垃圾,这就是专用 JWT 解码器击败手动解码令牌的原因。
如何调试 401 未经授权的响应?
Token 向外工作,解码后检查 exp 针对当前时间 - 过期的令牌是最常见的原因,并且在您将声明转换为可读日期之前它是不可见的。如果令牌已存活,请检查 Authorization header 本身:方案必须正确存在并拼写(Bearer <token>、(一个空格,没有引号),而从终端粘贴的令牌往往带有一条尾随换行符,打破匹配,之后,确认 aud 並 iss 声明符合 API 的预期,因为为不同受众发出的有效令牌被拒绝就像拒绝一个糟糕的令牌一样。然后才开始怀疑服务器。
如何解码没有库的 JWT?
JWT 是由点连接的三个 base64url 段。在点上拆分,然后 base64url-解码前两个 - 标头和有效负载 - 并且都以普通 JSON 的形式出现。第三个段是签名,它不会解码为任何可读的东西,因为它是原始字节而不是文本。这比听起来更重要:解码令牌告诉你它声称什么,而不是那些声明是否真实。验证签名需要发行者's 密钥,并且属于你的服务器代码,永远不要在浏览器工具中读取令牌来调试;在应用程序中验证它们。
如果我使用这些工具,我还需要邮递员还是失眠?
是- 他们解决了不同的问题。一个API客户端发送请求;这些工具使响应清晰可辨。在实践中,我使用客户端启动请求并复制原始输出,然后移动到浏览器工具格式化,解码,转换,并区分它们在工作流中彼此相邻而不是相互替换。



