ID 解码台

粘一串东西进来 —— 时间戳、MongoDB ObjectId、UUID、26 位 Base32、13 位 TSID、JWT、URL 编码的文本、JSON —— 它自己认是哪一种,把藏在里面的时间按东八区摆出来,并告诉你时间藏在哪几位上。

全部在浏览器里跑,不上传任何数据,断网也能用。位布局逐条对着 uuid-creator 6.1.0 与 tsid-creator 5.2.6 的源码实现,并用这两个库生成的样本做了 110 条回归测试。

此刻 (点毫秒可复制)
试试:

时间 → 时间戳

反着来。填进去的时刻一律当东八区,不跟着你机器的时区跑。

文本 → URL 编码

三种口径一起给 —— 用错口径正是「参数里的斜杠丢了」「空格变成加号」的来源。反向解码在上面那个框里粘就行。

时间到底藏在 ID 的哪里

这几种 ID 天天见,但「它里面有没有时间、时间从哪年算起」每次都得重新查一遍。下面是一次说清。

类型长相时间在哪纪元精度
Unix 时间戳10 / 13 / 16 / 19 位数字就是它本身1970-01-01秒 / 毫秒 / 微秒 / 纳秒
ObjectId24 位十六进制前 4 字节1970-01-01
UUID v736 位带横线前 48 位1970-01-01毫秒
UUID v136 位带横线三段拼起来的 60 位1582-10-15100 纳秒
UUID v436 位带横线没有时间
26 位 Base32a–z 2–7,小写前 10 个字符1970-01-01毫秒
13 位 TSIDCrockford Base32高 42 位2020-01-01毫秒
JWT三段点分载荷里的 exp / iat / nbf1970-01-01

唯一一个纪元不是 1970 的是 TSID。把它的 42 位时间当成 Unix 毫秒去解,会得到 1970 年前后的日期 —— 差着整整 50 年,这是踩得最多的一个坑。

时间戳:位数就是单位

没有哪个地方写着「这串数字是秒还是毫秒」,只能看长度:当下这个年代,秒是 10 位,毫秒 13 位,微秒 16 位,纳秒 19 位。所以 17866047951786604795330 是同一个时刻,差的只是单位。

把毫秒当秒解,会得到五万年后的日期;把秒当毫秒解,会落在 1970 年 1 月。上面的解码台碰到这种情况会直接把结果标黄提醒你换单位。

东八区不等于「本机时区」。页面上一律先给东八区(UTC+8,中国 1991 年后没有夏令时),你的机器时区如果不是 UTC+8 会另起一行列出来 —— 排查线上问题时,日志多半是 UTC,人看的是东八区,两个都要摆出来才不会算错八小时。

ObjectId:前 4 个字节就是秒

MongoDB 的 _id 是 12 字节:4 字节秒级时间 + 5 字节随机值(进程标识)+ 3 字节自增计数器。所以拿到一个 ObjectId 就等于拿到了这条文档的插入时刻,精确到秒 —— 同一秒内的先后靠最后那 3 字节的计数器分。

反过来说,ObjectId 的前 8 位十六进制是可预测的:别拿它当不可猜的凭证,也别指望它能隐藏「这条数据是什么时候建的」。

UUID:v4 里真的没有时间

UUID 的第 13 个十六进制位是版本号,它决定了这串东西里有没有时间:

v4 是纯随机,里面没有任何时间信息 —— 「帮我看看这个 UUID 是什么时候生成的」在 v4 上无解,不是工具不行,是它本来就没写进去。

v7 把 Unix 毫秒放在最前面的 48 位,所以它按字符串排序就等于按时间排序,做数据库主键时插入位置集中在 B-tree 右端,比 v4 友好得多。

v1 的纪元是 1582 年 10 月 15 日(公历改革日),单位是 100 纳秒,而且时间被拆成 low / mid / high 三段分散在串里 —— 所以 v1 的字符串顺序和时间顺序对不上,这也正是后来有了 v6、v7 的原因。

26 位 Base32 与 13 位 TSID

这两种是后端项目里常见的短 ID 写法,本质都是「把二进制换个更短的字母表写出来」:

26 位 Base32(uuid-creator 的 Base32Codec)就是一个 UUID v7 换成 RFC 4648 的 32 字母表(a–z 2–7)写出来,128 位塞进 26 个字符、每字符 5 位共 130 位,多出来的 2 位补在末尾 —— 这也是为什么它的最后一个字符只可能是 a / e / i / m / q / u / y / 4。时间在前 48 位,也就是前 10 个字符(第 10 个字符只有高 3 位属于时间)。

13 位 TSID 是 64 位整数换成 Crockford Base32(字母表里没有 i、l、o、u,就是为了不被人眼读错)。高 42 位是时间,低 22 位是节点号加同毫秒内的计数器。它的纪元是 2020-01-01,换算回 Unix 毫秒要加 1577836800000

这两种 ID 都不能截断。截掉几位既丢掉了版本位和随机位(撞号风险陡增),也再没法还原成原来的 UUID —— 需要更短的 ID 就换生成方式,不要在存的时候切一刀。

JWT:解码不等于验签

JWT 的三段是 头部.载荷.签名,前两段只是 base64url 编码的 JSON,任何人拿到都能直接读 —— 所以载荷里别放密码、身份证号这类东西,它不是密文。

签名那段能证明「令牌没被改过」,但验证它需要密钥。这个页面只解码、不验签:把密钥粘进任何网页去验,本身就是更危险的事。要验请在自己的服务端验。

载荷里的 exp / iat / nbf 按规范是,不是毫秒。直接拿 Date.now()(毫秒)去比大小,是「令牌明明没过期却被判过期」的经典原因。

JSON:格式化不该改内容

粘一段 JSON 进来会直接排版好,缩进 2 空格 / 4 空格 / 压缩成一行随手切。这里有一处和别处不一样:排版走的是逐字符重排,不是 JSON.parseJSON.stringify

因为 JSON 的数字在 JavaScript 里一律读成双精度浮点,能精确表示的整数上限是 9007199254740991(253−1)。一个 19 位的雪花 ID 或 Java long 主键读进来,末几位就悄悄变了 —— 再 stringify 出去,你拿到的是一份「看起来一样、ID 已经不是那个 ID」的 JSON。用它去查库,查不到。

所以这一页碰到超范围的整数会直接把它们列出来点名。根治办法是让这类字段以字符串传输(后端序列化时把 long 转成 string),而不是在前端想办法救。

解析不过时会给出出错的行列和前后 30 个字符。最常见的三种错法 —— 尾逗号、键没加双引号、字符串用单引号 —— 在 JavaScript 里全都合法,在 JSON 里全都不合法,这也是手写 JSON 最容易翻车的地方。另外从日志里抄出来的 JSON 常常是被转义成一整个字符串的(满屏 \"),直接粘进来就行,会先脱掉一层。

URL 编码:三种口径别用混

encodeURIComponent 转义包括 / ? & = # 在内的所有结构字符,放进查询参数的值里要用它encodeURI 保留结构字符,是给整条 URL 用的。把参数值用 encodeURI 编码,值里的 & 会原样留下,后端一拆参数就串位了。

表单和查询串(application/x-www-form-urlencoded)里空格写成 +,而在路径里 + 就是加号本身 —— 同一串 a+b 到底是「a 空格 b」还是「a 加 b」,取决于它在 URL 的哪个位置。上面的解码台碰到带 + 的串会把两种解法都列出来。

看到 %2520 说明被编码了两次(%20 里的 % 又被编成了 %25)—— 多半是某一层框架已经编过一次,代码里又编了一次。