分享链接是如何暴露分享者的

分享链接除了内容 ID,还可能携带由分享账号生成的参数。这里只记三种已经通过样本回算确认的算法:小红书 shareRedId、B站 vd_source 和抖音 u_code。

真实样本中的短链码、哈希、UID、sec_uid 和主页地址统一用 ****** 占位。代码里的 UID 是人工构造的演示值,不对应这些样本中的账号。

小红书:shareRedId 可逆编码

样本

原始短链
https://xhslink.cn/o/<短链码>

跳转目标中的身份字段
shareRedId=******

xhslink.cn 和 xhslink.com 的路径只是重定向索引。先跟随 HTTP Location 取得小红书长链接,再读取其中的 shareRedId。

编码原理

小红书用户 ID 是 24 位十六进制字符串。样本中的 shareRedId 按以下步骤生成:

  1. 将用户 ID 转为大写。
  2. 将每个字符的 ASCII 码分别加上固定偏移量。
  3. 对得到的 24 字节字符串执行 Base64URL 编码并删除末尾 =。

固定偏移量:

[2, 6, 2, 0, 3, 5, 4, 9, 6, 7, 5, 2, 9, 8, 0, 6, 6, 3, 9, 7, 4, 5, 6, 9]

解码是上述过程的逆运算:

decoded = Base64URLDecode(shareRedId)
uid[i] = chr(ord(decoded[i]) - shifts[i])

还原实现

import base64
import re

SHIFTS = [
    2, 6, 2, 0, 3, 5, 4, 9, 6, 7, 5, 2,
    9, 8, 0, 6, 6, 3, 9, 7, 4, 5, 6, 9,
]


def base64url_decode(value):
    padding = "=" * (-len(value) % 4)
    data = base64.urlsafe_b64decode(value + padding)
    return data.decode("latin1")


def base64url_encode(value):
    data = base64.urlsafe_b64encode(value.encode("latin1"))
    return data.decode("ascii").rstrip("=")


def decode_share_red_id(share_red_id):
    transformed = base64url_decode(share_red_id)
    if len(transformed) != len(SHIFTS):
        raise ValueError("unexpected shareRedId length")

    uid = "".join(
        chr(ord(char) - SHIFTS[index])
        for index, char in enumerate(transformed)
    ).lower()

    if not re.fullmatch(r"[0-9a-f]{24}", uid):
        raise ValueError("decoded value is not a 24-character user ID")
    return uid


def encode_share_red_id(uid):
    if not re.fullmatch(r"[0-9a-fA-F]{24}", uid):
        raise ValueError("invalid user ID")

    transformed = "".join(
        chr(ord(char) + SHIFTS[index])
        for index, char in enumerate(uid.upper())
    )
    return base64url_encode(transformed)

验证

使用人工构造的 UID:

uid
0123456789abcdef01234567

逐位加偏移后的字符串
27437::@>@FDLLEL64;:8:<@

Base64URL 编码
Mjc0Mzc6OkA-QEZETExFTDY0Ozo4OjxA

将最后一行交给 decode_share_red_id,会恢复为 0123456789abcdef01234567。实际样本同样执行“解码、格式检查、重新编码”三步校验,但不在文章中公开其身份值。

B站:vd_source 是 UID 的 MD5

样本

样本里的 vd_source 覆盖了两种算法。

标准字符串 MD5 样本
https://www.bilibili.com/video/BV1hngA6XErb/?share_source=copy_web&vd_source=******

数字字节 MD5 样本
https://www.bilibili.com/video/BV1JDJZzAExt?vd_source=******

b23.tv 短链需要先跟随 HTTP Location 取得 bilibili.com 长链接,再读取 vd_source。

编码原理

样本中存在两种 UID 序列化方式。

第一种直接对 UID 的十进制 ASCII 字符串计算 MD5:

A(uid) = ASCII(String(uid))
vd_source = MD5(A(uid))

第二种先把十进制字符串的每一位转换为数值字节,再计算 MD5:

B(uid) = [Number(digit) for digit in String(uid)]
vd_source = MD5(B(uid))

例如字符串 "12345678" 的两种字节表示为:

ASCII 字节: 31 32 33 34 35 36 37 38
数字字节:  01 02 03 04 05 06 07 08

MD5 不可逆,但数字 UID 是有限候选空间,因此可以通过原像搜索找到匹配 UID。

还原实现

import hashlib


def md5(data):
    return hashlib.md5(data).hexdigest()


def standard_vd_source(uid):
    return md5(str(uid).encode("ascii"))


def digit_bytes_vd_source(uid):
    data = bytes(int(digit) for digit in str(uid))
    return md5(data)


def find_uid(target, max_uid=999_999_999):
    for uid in range(1, max_uid + 1):
        if standard_vd_source(uid) == target:
            return uid, "ascii"
        if digit_bytes_vd_source(uid) == target:
            return uid, "digit-bytes"
    return None

搜索复杂度为 O(N)。可以把连续 UID 区间分给多个线程并行计算,但并行只改变速度,不改变匹配结果。

验证

使用人工构造的 UID 12345678:

MD5(ASCII("12345678"))
= 25d55ad283aa400af464c76d713c07ad

MD5([1, 2, 3, 4, 5, 6, 7, 8])
= 0ee0646c1c77d8131cc8f4ee65c7673b

把任一哈希交给 find_uid,都会返回 UID 12345678 和对应的序列化方式。真实的两条样本分别命中这两个分支,其哈希和还原 UID 使用 ****** 占位。

抖音:u_code 是 UID 的 23 进制编码

样本

视频短链
https://v.douyin.com/<短链码>/

第一跳 activity_info
u_code=******
social_share_user_id=******
social_author_id=******

直播短链
https://v.douyin.com/<短链码>/

第一跳参数
u_code=******
share_user_id=******
sec_relation_user_id=******

v.douyin.com 的路径是重定向索引。跟随第一跳 HTTP Location 后,需要同时读取普通查询参数和 URL 编码 JSON 中的 activity_info、share_extra_params、ecom_share_track_params。

编码原理

u_code 使用 23 个字符表示数字 UID:

字符: 0 1 2 3 4 5 6 7 8 9 a b c d e f g h i j k l m
数值: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

从左向右按普通进位制累加:

uid = 0
uid = uid * 23 + value(char)

u_code 可以还原数字 UID,但不能据此计算 sec_uid。如果同一跳转地址同时包含 share_user_id 和 sec_relation_user_id,只能说明该链接直接给出了两者的对应关系。

还原实现

ALPHABET = "0123456789abcdefghijklm"


def decode_douyin_u_code(u_code):
    uid = 0
    for char in u_code.lower():
        digit = ALPHABET.find(char)
        if digit < 0:
            raise ValueError("invalid u_code")
        uid = uid * 23 + digit
    return uid


def encode_douyin_uid(uid):
    uid = int(uid)
    if uid == 0:
        return "0"

    result = ""
    while uid > 0:
        uid, digit = divmod(uid, 23)
        result = ALPHABET[digit] + result
    return result

Python 整数支持任意精度,可以直接处理长 UID。

验证

使用人工构造的 UID:

uid
1234567890

23 进制 u_code
87ifcgi

encode_douyin_uid(1234567890) 得到 87ifcgi,再执行 decode_douyin_u_code("87ifcgi") 会恢复为 1234567890。

真实视频样本中,u_code 的解码结果与 activity_info.social_share_user_id 完全一致;social_author_id 是内容作者 UID,不参与计算。真实直播样本中,u_code 的解码结果与 share_user_id 一致,并且同一跳转地址另外给出了对应的 sec_relation_user_id。这些身份值均用 ****** 占位。

其他字段的算法分类

结果类型 示例字段 可推导结论
明文用户 ID uid、shareuid、shareUid、appuid 格式通过后可直接得到用户标识
可逆编码或加密 小红书 shareRedId、网易云 uct2 解码后可通过反向计算或格式校验
进位制编码 抖音 u_code 按 23 进制还原数字 UID
有限空间哈希 B站 vd_source 枚举候选并精确匹配原始哈希
不透明标识 hosteuin、ruk、utm_oi、sec_sharer_id 只能比较是否相同,不能直接还原公开 UID

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。

文章标题:分享链接是如何暴露分享者的

字数:1.5k

本文作者:

发布时间:2026-08-24, 06:30:00

最后更新:2026-08-26, 21:18:00

原始链接:https://cnlnn.pages.dev/posts/share-link-identity-tracking/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。