# Mercari DPoP 参数分析文档 ## 一、什么是 DPoP DPoP(Demonstrating Proof-of-Possession)是 **RFC 9449** 定义的标准协议,用于将请求绑定到特定客户端的密钥对上。Mercari API 使用 DPoP 作为请求鉴权的一部分。 DPoP Token 本质是一个 **JWT**(JSON Web Token),格式为 `header.payload.signature`,三段 Base64URL 编码。 --- ## 二、Token 结构解析 ### 2.1 Header(两个接口相同) ```json { "typ": "dpop+jwt", "alg": "ES256", "jwk": { "crv": "P-256", "kty": "EC", "x": "j9I6kLKekdSNdHyHsaZl5gkbbFhDaAP3DwsuvVjCrWg", "y": "NLtDkddYVafFykQGbk-x6AaJzAjUnVepYt_jsvewpgI" } } ``` | 字段 | 值 | 说明 | |------|----|------| | `typ` | `dpop+jwt` | 标识为 DPoP 类型的 JWT | | `alg` | `ES256` | 签名算法:ECDSA + P-256 曲线 + SHA-256 | | `jwk` | EC 公钥 | 内嵌椭圆曲线公钥,服务端用来验证签名 | ### 2.2 Payload(两个接口不同) **LIST_DPOP**(搜索列表接口): ```json { "iat": 1778046219, "jti": "44bc836e-abaa-4258-a248-3e691153f665", "htu": "https://api.mercari.jp/v2/entities:search", "htm": "POST", "uuid": "a00429c5-ad26-4be4-83ae-60b7239e14d5" } ``` **DETAIL_DPOP**(商品详情接口): ```json { "iat": 1778046009, "jti": "1f4f049a-7f0f-4c4f-af71-20baad8a1784", "htu": "https://api.mercari.jp/items/get", "htm": "GET", "uuid": "a00429c5-ad26-4be4-83ae-60b7239e14d5" } ``` ### 2.3 Payload 字段说明 | 字段 | 说明 | LIST 值 | DETAIL 值 | |------|------|---------|-----------| | `iat` | 签发时间(Unix 时间戳) | `1778046219` | `1778046009` | | `jti` | JWT ID(UUID v4 随机值,防重放) | `44bc836e-...` | `1f4f049a-...` | | `htu` | HTTP Target URI(目标接口地址) | `https://api.mercari.jp/v2/entities:search` | `https://api.mercari.jp/items/get` | | `htm` | HTTP Method(请求方法) | `POST` | `GET` | | `uuid` | 设备标识(与 `LAPLACE_DEVICE_UUID` 相同) | `a00429c5-...` | `a00429c5-...` | ### 2.4 Signature 使用 EC 私钥对 `header_base64url.payload_base64url` 进行 **ES256** 签名,输出 64 字节的 raw `r||s` 格式(各 32 字节),再做 Base64URL 编码。 --- ## 三、两个关键问题 ### 3.1 为什么会过期? `iat`(issued at)字段是一个**固定的时间戳**。服务端会校验该时间与当前时间的差值,超过有效期窗口(通常几天)就会拒绝请求。硬编码的 token 中 `iat` 永远不变,所以过几天就失效。 ### 3.2 为什么 LIST 和 DETAIL 不一样? DPoP 规范要求 token 绑定到**具体的接口和方法**: | 差异字段 | LIST(搜索列表) | DETAIL(商品详情) | |----------|-----------------|-------------------| | `htu` | `/v2/entities:search` | `/items/get` | | `htm` | `POST` | `GET` | | `jti` | 各自独立的随机 UUID | 各自独立的随机 UUID | | `iat` | 各自的签发时间 | 各自的签发时间 | 服务端会验证 `htu` 和 `htm` 是否匹配实际请求,所以两个接口不能共用同一个 DPoP token。 --- ## 四、生成流程 ``` 1. 生成 EC P-256 密钥对(私钥 + 公钥) ↓ 2. 构造 JWT Header(typ + alg + 公钥 jwk) ↓ 3. 构造 JWT Payload(iat=当前时间戳, jti=随机UUID, htu=目标URL, htm=方法, uuid=设备ID) ↓ 4. 拼接 base64url(header).base64url(payload) ↓ 5. 用私钥对拼接字符串做 ES256 签名 ↓ 6. 输出 base64url(header).base64url(payload).base64url(signature) ``` --- ## 五、Python 实现 已封装在 `dpop_generator.py` 中,依赖 `cryptography` 库。 ### 5.1 快速使用 ```python from dpop_generator import generate_list_dpop, generate_detail_dpop # 搜索列表接口 list_dpop = generate_list_dpop() # 商品详情接口 detail_dpop = generate_detail_dpop() ``` ### 5.2 自定义设备 UUID ```python from dpop_generator import DPoPGenerator gen = DPoPGenerator(device_uuid="your-device-uuid") list_dpop = gen.generate(htu="https://api.mercari.jp/v2/entities:search", htm="POST") detail_dpop = gen.generate(htu="https://api.mercari.jp/items/get", htm="GET") ``` ### 5.3 在爬虫中集成 将 `mercari_jp_spider.py` 中的硬编码常量替换为动态调用: ```python from dpop_generator import generate_list_dpop, generate_detail_dpop # build_headers() 中: "dpop": generate_list_dpop(), # get_detail_page() 中: "dpop": generate_detail_dpop(), ``` 每次调用自动生成新的 `iat` 和 `jti`,不会再过期。同一个 `DPoPGenerator` 实例复用同一密钥对,保证签名一致性。 --- ## 六、依赖安装 ```bash pip install cryptography ```