From 05ac3d5e2a9e28be3bf129ae8c78ffdbebaa161c Mon Sep 17 00:00:00 2001 From: SocialSisterYi <1440239038@qq.com> Date: Tue, 23 May 2023 09:38:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E6=A1=A3=E3=80=90Wb?= =?UTF-8?q?i=20=E6=8E=A5=E5=8F=A3=E7=AD=BE=E5=90=8D=E3=80=91=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=9B=AE=E5=BD=95=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 +- docs/login/login_info.md | 204 ++++++++++++++--------- docs/{other => misc}/bvid_desc.md | 0 docs/{other => misc}/errcode.md | 0 docs/{other => misc}/picture.md | 0 docs/misc/sign/APP.md | 65 ++++++++ docs/misc/sign/APPKey.md | 54 +++++++ docs/misc/sign/wbi.md | 250 +++++++++++++++++++++++++++++ docs/{other => misc}/time_stamp.md | 0 docs/other/API_sign.md | 93 ----------- 10 files changed, 509 insertions(+), 173 deletions(-) rename docs/{other => misc}/bvid_desc.md (100%) rename docs/{other => misc}/errcode.md (100%) rename docs/{other => misc}/picture.md (100%) create mode 100644 docs/misc/sign/APP.md create mode 100644 docs/misc/sign/APPKey.md create mode 100644 docs/misc/sign/wbi.md rename docs/{other => misc}/time_stamp.md (100%) delete mode 100644 docs/other/API_sign.md diff --git a/README.md b/README.md index 9a74ea0..d12eeaa 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,14 @@ B站 API 采用 C/S 结构,大多数接口为 REST API 和 gRPC,少部分接 计划整理分类 & 目录:(文档已完结请选中 checkbox) -- [x] [API 签名](docs/other/API_sign.md) -- [x] [公共错误码](docs/other/errcode.md) -- [x] [图片格式化](docs/other/picture.md) -- [x] [bvid 说明](docs/other/bvid_desc.md) +- [ ] [接口签名与验证](docs/misc/sign) + - [x] [APP API 签名](docs/misc/sign/APP.md)(`appkey`与`sign`) + - [x] [已知的 APPKey](docs/misc/sign/APPKey.md) + - [x] [Wbi 签名](docs/misc/sign/wbi.md)(`wts`与`w_rid`) + +- [x] [公共错误码](docs/misc/errcode.md) +- [x] [图片格式化](docs/misc/picture.md) +- [x] [bvid 说明](docs/misc/bvid_desc.md) - [ ] [gRPC API 接口定义](grpc_api) - [ ] [登录](docs/login) - [x] [登录操作 (人机认证)](docs/login/login_action) @@ -234,8 +238,8 @@ B站 API 采用 C/S 结构,大多数接口为 REST API 和 gRPC,少部分接 - [ ] [终端网络查询](docs/clientinfo) - [x] [基于ip的地理位置查询](docs/clientinfo/ip.md) - [x] [终端信息查询](docs/clientinfo/client_info.md) -- [ ] [其他](docs/other) - - [x] [获取当前时间戳](docs/other/time_stamp.md) +- [ ] [其他](docs/misc) + - [x] [获取当前时间戳](docs/misc/time_stamp.md) - [ ] [web端组件](docs/web_widget) - [x] [分区当日投稿数](docs/web_widget/zone_upload.md) - [x] [404 页漫画收集](docs/web_widget/404_manga.md) diff --git a/docs/login/login_info.md b/docs/login/login_info.md index 57925c3..c48b705 100644 --- a/docs/login/login_info.md +++ b/docs/login/login_info.md @@ -27,30 +27,33 @@ | -------------------- | ---- | ---------------- | ------------------------------------------------- | | isLogin | bool | 是否已登录 | false:未登录
true:已登录 | | email_verified | num | 是否验证邮箱地址 | 0:未验证
1:已验证 | -| face | str | 用户头像url | | +| face | str | 用户头像 url | | | level_info | obj | 等级信息 | | -| mid | num | 用户mid | | +| mid | num | 用户 mid | | | mobile_verified | num | 是否验证手机号 | 0:未验证
1:已验证 | | money | num | 拥有硬币数 | | | moral | num | 当前节操值 | 上限为70 | | official | obj | 认证信息 | | -| officialVerify | obj | 认证信息2 | | +| officialVerify | obj | 认证信息 2 | | | pendant | obj | 头像框信息 | | -| scores | num | 0 | 作用尚不明确 | +| scores | num | (?) | | | uname | str | 用户昵称 | | | vipDueDate | num | 会员到期时间 | 毫秒 时间戳 | | vipStatus | num | 会员开通状态 | 0:无
1:有 | | vipType | num | 会员类型 | 0:无
1:月度大会员
2:年度及以上大会员 | | vip_pay_type | num | 会员开通状态 | 0:无
1:有 | -| vip_theme_type | num | 0 | 作用尚不明确 | +| vip_theme_type | num | (?) | | | vip_label | obj | 会员标签 | | | vip_avatar_subscript | num | 是否显示会员图标 | 0:不显示
1:显示 | | vip_nickname_color | str | 会员昵称颜色 | 颜色码 | | wallet | obj | B币钱包信息 | | | has_shop | bool | 是否拥有推广商品 | false:无
true:有 | -| shop_url | str | 商品推广页面url | | -| allowance_count | num | 0 | 作用尚不明确 | -| answer_status | num | 0 | 作用尚不明确 | +| shop_url | str | 商品推广页面 url | | +| allowance_count | num | (?) | | +| answer_status | num | (?) | | +| is_senior_member | num | 是否硬核会员 | 0:非硬核会员
1:硬核会员 | +| wbi_img | obj | Wbi 签名实时口令 | 该字段即使用户未登录也存在 | +| is_jury | bool | (?) | | `data`中的`level_info`对象: @@ -79,35 +82,42 @@ `data`中的`pendant`对象: -| 字段 | 类型 | 内容 | 备注 | -| ------ | ---- | ----------- | ------------ | -| pid | num | 挂件id | | -| name | str | 挂件名称 | | -| image | str | 挂件图片url | | -| expire | num | 0 | 作用尚不明确 | +| 字段 | 类型 | 内容 | 备注 | +| ------ | ---- | ----------- | ---- | +| pid | num | 挂件id | | +| name | str | 挂件名称 | | +| image | str | 挂件图片url | | +| expire | num | (?) | | `data`中的`vip_label`对象: | 字段 | 类型 | 内容 | 备注 | | ----------- | ---- | -------- | ------------------------------------------------------------ | -| path | str | 空 | 作用尚不明确 | +| path | str | (?) | | | text | str | 会员名称 | | | label_theme | str | 会员标签 | vip:大会员
annual_vip:年度大会员
ten_annual_vip:十年大会员
hundred_annual_vip:百年大会员 | `data`中的`wallet`对象: -| 字段 | 类型 | 内容 | 备注 | -| --------------- | ---- | ------------- | ------------ | -| mid | num | 登录用户mid | | -| bcoin_balance | num | 拥有B币数 | | -| coupon_balance | num | 每月奖励B币数 | | -| coupon_due_time | num | 0 | 作用尚不明确 | +| 字段 | 类型 | 内容 | 备注 | +| --------------- | ---- | ------------- | ---- | +| mid | num | 登录用户mid | | +| bcoin_balance | num | 拥有B币数 | | +| coupon_balance | num | 每月奖励B币数 | | +| coupon_due_time | num | (?) | | + +`data`中的`wbi_img`对象: + +| 字段 | 类型 | 内容 | 备注 | +| ------- | ---- | ------------------------------- | ---------------------------------------- | +| img_url | str | Wbi 签名参数 `imgKey`的伪装 url | 详见文档 [Wbi 签名](../misc/sign/wbi.md) | +| sub_url | str | Wbi 签名参数 `subKey`的伪装 url | 详见文档 [Wbi 签名](../misc/sign/wbi.md) | **示例:** ```shell curl 'https://api.bilibili.com/nav' \ --b 'SESSDATA=xxx' + -b 'SESSDATA=xxx' ```
@@ -115,64 +125,110 @@ curl 'https://api.bilibili.com/nav' \ ```json { - "code":0, - "message":"0", - "ttl":1, - "data":{ - "isLogin":true, - "email_verified":1, - "face":"http://i1.hdslb.com/bfs/face/aebb2639a0d47f2ce1fec0631f412eaf53d4a0be.jpg", - "level_info":{ - "current_level":5, - "current_min":10800, - "current_exp":17065, - "next_exp":28800 + "code": 0, + "message": "0", + "ttl": 1, + "data": { + "isLogin": true, + "email_verified": 1, + "face": "https://i0.hdslb.com/bfs/face/aebb2639a0d47f2ce1fec0631f412eaf53d4a0be.jpg", + "face_nft": 0, + "face_nft_type": 0, + "level_info": { + "current_level": 6, + "current_min": 28800, + "current_exp": 52689, + "next_exp": "--" }, - "mid":293793435, - "mobile_verified":1, - "money":33.4, - "moral":70, - "official":{ - "role":0, - "title":"", - "desc":"", - "type":-1 + "mid": 293793435, + "mobile_verified": 1, + "money": 172.4, + "moral": 70, + "official": { + "role": 0, + "title": "", + "desc": "", + "type": -1 }, - "officialVerify":{ - "type":-1, - "desc":"" + "officialVerify": { + "type": -1, + "desc": "" }, - "pendant":{ - "pid":0, - "name":"", - "image":"", - "expire":0, - "image_enhance":"" + "pendant": { + "pid": 2511, + "name": "初音未来13周年", + "image": "https://i0.hdslb.com/bfs/garb/item/4f8f3f1f2d47f0dad84f66aa57acd4409ea46361.png", + "expire": 0, + "image_enhance": "https://i0.hdslb.com/bfs/garb/item/fe0b83b53e2342b16646f6e7a9370d8a867decdb.webp", + "image_enhance_frame": "https://i0.hdslb.com/bfs/garb/item/127c507ec8448be30cf5f79500ecc6ef2fd32f2c.png" }, - "scores":0, - "uname":"社会易姐QwQ", - "vipDueDate":1612454400000, - "vipStatus":1, - "vipType":2, - "vip_pay_type":1, - "vip_theme_type":0, - "vip_label":{ - "path":"", - "text":"年度大会员", - "label_theme":"annual_vip" + "scores": 0, + "uname": "社会易姐QwQ", + "vipDueDate": 1707494400000, + "vipStatus": 1, + "vipType": 2, + "vip_pay_type": 0, + "vip_theme_type": 0, + "vip_label": { + "path": "", + "text": "年度大会员", + "label_theme": "annual_vip", + "text_color": "#FFFFFF", + "bg_style": 1, + "bg_color": "#FB7299", + "border_color": "", + "use_img_label": true, + "img_label_uri_hans": "", + "img_label_uri_hant": "", + "img_label_uri_hans_static": "https://i0.hdslb.com/bfs/vip/8d4f8bfc713826a5412a0a27eaaac4d6b9ede1d9.png", + "img_label_uri_hant_static": "https://i0.hdslb.com/bfs/activity-plat/static/20220614/e369244d0b14644f5e1a06431e22a4d5/VEW8fCC0hg.png" }, - "vip_avatar_subscript":1, - "vip_nickname_color":"#FB7299", - "wallet":{ - "mid":293793435, - "bcoin_balance":8, - "coupon_balance":5, - "coupon_due_time":0 + "vip_avatar_subscript": 1, + "vip_nickname_color": "#FB7299", + "vip": { + "type": 2, + "status": 1, + "due_date": 1707494400000, + "vip_pay_type": 0, + "theme_type": 0, + "label": { + "path": "", + "text": "年度大会员", + "label_theme": "annual_vip", + "text_color": "#FFFFFF", + "bg_style": 1, + "bg_color": "#FB7299", + "border_color": "", + "use_img_label": true, + "img_label_uri_hans": "", + "img_label_uri_hant": "", + "img_label_uri_hans_static": "https://i0.hdslb.com/bfs/vip/8d4f8bfc713826a5412a0a27eaaac4d6b9ede1d9.png", + "img_label_uri_hant_static": "https://i0.hdslb.com/bfs/activity-plat/static/20220614/e369244d0b14644f5e1a06431e22a4d5/VEW8fCC0hg.png" + }, + "avatar_subscript": 1, + "nickname_color": "#FB7299", + "role": 3, + "avatar_subscript_url": "", + "tv_vip_status": 0, + "tv_vip_pay_type": 0, + "tv_due_date": 1640793600 }, - "has_shop":false, - "shop_url":"", - "allowance_count":0, - "answer_status":0 + "wallet": { + "mid": 293793435, + "bcoin_balance": 5, + "coupon_balance": 5, + "coupon_due_time": 0 + }, + "has_shop": true, + "shop_url": "https://gf.bilibili.com?msource=main_station", + "allowance_count": 0, + "answer_status": 0, + "is_senior_member": 1, + "wbi_img": { + "img_url": "https://i0.hdslb.com/bfs/wbi/653657f524a547ac981ded72ea172057.png", + "sub_url": "https://i0.hdslb.com/bfs/wbi/6e4909c702f846728e64f6007736a338.png" + }, + "is_jury": false } } ``` diff --git a/docs/other/bvid_desc.md b/docs/misc/bvid_desc.md similarity index 100% rename from docs/other/bvid_desc.md rename to docs/misc/bvid_desc.md diff --git a/docs/other/errcode.md b/docs/misc/errcode.md similarity index 100% rename from docs/other/errcode.md rename to docs/misc/errcode.md diff --git a/docs/other/picture.md b/docs/misc/picture.md similarity index 100% rename from docs/other/picture.md rename to docs/misc/picture.md diff --git a/docs/misc/sign/APP.md b/docs/misc/sign/APP.md new file mode 100644 index 0000000..b2cc7c2 --- /dev/null +++ b/docs/misc/sign/APP.md @@ -0,0 +1,65 @@ +# APP API 签名与鉴权 + +## APP API 签名特性 + +部分客户端专用的 REST API 存在基于参数签名的鉴权,需要使用规定的`appkey`及其对应的`appsec`与原始请求参数进行签名计算,部分`AppKey`及与之对应的`AppSec`已经被公开:见该文档 [APPKey](APPKey.md) + +- 不同 `appkey` 对应不同的 app (如客户端、概念版、必剪、漫画、bililink等) + +- 不同平台同 app 也会存在不同的 `appkey` (如安卓端、ios端、TV端等) + +- 同平台同 app 下不同功能也会存在不同的 `appkey`(如登录专用、取流专用等) + +- 不同版本的客户端的 `appkey` 也可能不同 + +- **appkey与appsec一一对应** + +## APP API 签名算法 + +1. 首先为参数中添加`appkey`字段 +2. 然后按照参数的 Key 重新排序 +3. 再对这个 Key-Value 进行 url query 序列化,并拼接与之对应的`appsec` (盐) 进行 **md5 Hash 运算**(32-bit 字符小写),该 hash 便是 API 签名 +4. 最后在参数尾部增添`sign`字段,它的 Value 为上一步计算所得的 hash,一并作为表单或 Query 提交 + +## Demo + +该 Demo 提供 [Python](#Python) 语言例程 + +使用 appkey = `1d8b6e7d45233436`, appsec = `560c52ccd288fed045859ed18bffd973` 对如下 `params` 参数进行签名 + +上述示例`appkey`、`AppSec`均来自文档 [APPKey](APPKey.md) + +### Python + +```python +import hashlib +import urllib.parse + +def appsign(params, appkey, appsec): + '为请求参数进行 APP 签名' + params.update({'appkey': appkey}) + params = dict(sorted(params.items())) # 按照 key 重排参数 + query = urllib.parse.urlencode(params) # 序列化参数 + sign = hashlib.md5((query+appsec).encode()).hexdigest() # 计算 api 签名 + params.update({'sign':sign}) + return params + +appkey = '1d8b6e7d45233436' +appsec = '560c52ccd288fed045859ed18bffd973' +params = { + 'id':114514, + 'str':'1919810', + 'test':'いいよ,こいよ', +} +signed_params = appsign(params, appkey, appsec) +query = urllib.parse.urlencode(signed_params) +print(signed_params) +print(query) +``` + +输出内容分别是进行 APP 签名的后参数的 key-Value 以及 url query 形式 + +``` +{'appkey': '1d8b6e7d45233436', 'id': 114514, 'str': '1919810', 'test': 'いいよ,こいよ', 'sign': '01479cf20504d865519ac50f33ba3a7d'} +appkey=1d8b6e7d45233436&id=114514&str=1919810&test=%E3%81%84%E3%81%84%E3%82%88%EF%BC%8C%E3%81%93%E3%81%84%E3%82%88&sign=01479cf20504d865519ac50f33ba3a7d +``` diff --git a/docs/misc/sign/APPKey.md b/docs/misc/sign/APPKey.md new file mode 100644 index 0000000..ae64461 --- /dev/null +++ b/docs/misc/sign/APPKey.md @@ -0,0 +1,54 @@ +# APIKey + +以下为已知的 APPkey / APPSec,及部分使用场景参数信息,均来自抓包与逆向工程 + +| APPKEY | APPSEC | platform2 | APP类型 | neuronAppId1 | mobi_app2 | 备注 | +| :--------------: | :------------------------------: | :------------------: | :----------------: | :---------------------: | :------------------: | :----------------------------------------: | +| 9d5889cf67e615cd | 8fd9bb32efea8cef801fd895bef2713d | `android` | Ai4cCreatorAndroid | | | | +| 1d8b6e7d45233436 | 560c52ccd288fed045859ed18bffd973 | `android` | 粉版 | `1` | `android` | 获取资源通用 | +| 783bbb7264451d82 | 2653583c8873dea268ab9386918b1d65 | `android` | 粉版 | `1` | `android` | 仅获取用户信息时使用(7.X及更新版本) | +| 57263273bc6b67f6 | a0488e488d1567960d3a765e8d129f90 | `android` | 粉版 | `1` | `android` | 可能来自旧版 | +| 07da50c9a0bf829f | 25bdede4e1581c836cab73a48790ca6e | `android` | 概念版 | `3` | `android_b` | | +| 191c3b6b975af184 | | `android` | 概念版 | `3` | `android_b` | 新出现, 仅获取用户信息时使用. 暂未知appsec | +| 178cf125136ca8ea | 34381a26236dd1171185c0beb042e1c6 | `android` | 概念版 | `3` | `android_b` | 可能来自旧版 | +| 7d336ec01856996b | a1ce6983bc89e20a36c37f40c4f1a0dd | `android` | 概念版 | `3` | `android_b` | 可能来自旧版 | +| dfca71928277209b | b5475a8825547a4fc26c7d518eaaa02e | `android` | HD 版 | `5` | `android_hd` | | +| bb3101000e232e27 | 36efcfed79309338ced0380abd824ac1 | `android` | 白版 | `14` | `android_i` | | +| ae57252b0c09105d | c75875c596a69eb55bd119e74b07cfe3 | `android` | 白版 | `14` | `android_i` | 仅获取用户信息时使用(7.X及更新版本) | +| 8e16697a1b4f8121 | f5dd03b752426f2e623d7badb28d190a | `android` | 白版 | `14` | `android_i` | 可能来自旧版 | +| 7d089525d3611b1c | acd495b248ec528c2eed1e862d393126 | `android` | 蓝版 | `30` | `bstar_a` | | +| iVGUTjsxvpLeuDCf | aHRmhWMLkdeMuILqORnYZocwMBpMEOdt | `android` | - | - | - | 视频取流专用, 仅5.X旧版使用 | +| YvirImLGlLANCLvM | JNlZNgfNGKZEpaDTkCdPQVXntXhuiJEM | `ios` | - | - | - | 视频取流专用 | +| 27eb53fc9058f8c3 | c2ed53a74eeefe3cf99fbd01d8c9c375 | `web`/`ios`? | - | - | - | 第三方授权使用 | +| 84956560bc028eb7 | 94aba54af9065f71de72f5508f1cd42e | ? | UWP 版 | - | - | 部分API不接受此appkey, 返回-663错误 | +| 85eb6835b0a1034e | 2ad42749773c441109bdc0191257a664 | ? | UWP 版? | - | - | 部分API不接受此appkey, 返回-663错误 | +| 4ebafd7c4951b366 | 8cb98205e9b2ad3669aad0fce12a4c13 | `ios` | iPhone 客户端? | `iphone` | ? | | +| 8d23902c1688a798 | 710f0212e62bd499b8d3ac6e1db9302a | `android` | AndroidBiliThings | ? | ? | | +| 4c6e1021617d40d9 | e559a59044eb2701b7a8628c86aa12ae | `android` | AndroidMallTicket | ? | ? | | +| c034e8b74130a886 | e4e8966b1e71847dc4a3830f2d078523 | `android` | AndroidOttSdk | `7` | ? | | +| 4409e2ce8ffd12b8 | 59b43e04ad6965f34319062b478f83dd | `android` | 云视听小电视(TV版) | `9`? | `android_tv_yst`? | | +| 37207f2beaebf8d7 | e988e794d4d4b6dd43bc0e89d6e90c43 | `android` | BiliLink | ? | ? | | +| 9a75abf7de2d8947 | 35ca1c82be6c2c242ecc04d88c735f31 | `android` | BiliScan | ? | ? | | +| aae92bc66f3edfab | af125a0d5279fd576c1b4418a3e8276d | ? | PC 投稿工具 | - | ? | | +| bca7e84c2d947ac6 | 60698ba2f68e01ce44738920a0ffe768 | ? | login | - | ? | | + +注释: + +1 `neuronAppId`,产品编号,由数据平台分配,详情如下: + +- 粉(国内版)=1 +- 白(GooglePlay 版)=2 +- 蓝(东南亚版)=3 +- 直播姬=4 +- HD=5 +- 海外=6 +- OTT=7 +- 漫画=8 +- TV野版=9 +- 小视频=10 +- 网易漫画=11 +- 网易漫画lite=12 +- 网易漫画HD=13, +- 国际版=14 + +2 `platform`, `mobi_app` 仅供参考, 具体值需要抓包确定. \ No newline at end of file diff --git a/docs/misc/sign/wbi.md b/docs/misc/sign/wbi.md new file mode 100644 index 0000000..e1b1e8d --- /dev/null +++ b/docs/misc/sign/wbi.md @@ -0,0 +1,250 @@ +# Wbi签名 + +自 2023 年三月起,B站 Web 端部分接口开始使用 Wbi 鉴权方式,即一种独立于 [APP 鉴权](APP.md) 与其他 Cookie 鉴权的方式,表现在 REST API 请求时在 query 中添加了`w_rid`和`wts`字段,为一种 Web 端的风控手段 + +这些接口涵盖”用户投稿视频“、”用户投稿专栏“、”首页推送“、”推广信息“、”热搜“、”视频信息“、”视频取流“、”搜索“等待主要查询性业务接口,如果请求这些 REST API 缺失`w_rid`和`wts`字段,则会在数次请求后返回`-403:非法访问`这样的风控错误 + +感谢 [#631](https://github.com/SocialSisterYi/bilibili-API-collect/issues/631) 的研究与逆向工程 + +## Wbi签名算法 + +1. 获取实时口令 + + 从 [nav 接口](../../login/login_info.md#导航栏用户信息) 中获取`img_url`、`sub_url`两个字段的参数,并保存备用(如存入 localStorage),相关内容节选如下: + + **注:`img_url`、`sub_url`两个字段的值看似为存于 BFS 中的 png 图片 url,实则只是经过伪装的实时 Token,故无需且不能试图访问这两个 url** + + ```json + "wbi_img": { + "img_url": "https://i0.hdslb.com/bfs/wbi/653657f524a547ac981ded72ea172057.png", + "sub_url": "https://i0.hdslb.com/bfs/wbi/6e4909c702f846728e64f6007736a338.png" + }, + ``` + 这两个 Key 均为 url 中末尾路径的无扩展名的文件名,即`img_key=653657f524a547ac981ded72ea172057`,`sub_key=6e4909c702f846728e64f6007736a338` + + 这两个 Key 的值无关登录 Session 与 IP,属于全站统一使用的,但**每日都会变化**,使用时应做好**缓存和刷新**处理 + +2. 打乱重排实时口令 + + 把上一步获取到的`img_key`拼接在`sub_key`后面**(这里不是`img_url`和`sub_url`)**作为一个整体,将这个整体进行特定的顺序的字符打乱重排,再将重排后的字符串截取前 30 字符的切片,作为一个新的变量`mixin_key`,重排映射表长为 64,内容如下: + + ```javascript + const mixinKeyEncTab = [ + 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, + 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, + 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, + 36, 20, 34, 44, 52 + ] + ``` + + 打乱重排内容如下(以上述第 1 步的参数作为输入) + + ``` + 72136226c6a73669787ee4fd02a74c27 + ``` + +3. 将欲签名的请求参数排序后编码 + + 若下方内容为欲签名的请求参数(以 js obj 为例) + + ```javascript + { + foo: '114', + bar: '514', + baz: 1919810 + } + ``` + + 那么按照 Key 排序并进行 url query 编码后的结果应为: + + ``` + bar=514&baz=1919810&foo=114 + ``` + +4. 为参数中添加`wts`时间戳 + + `wts`字段的值应为以秒为单位的 Unix TimeStamp,如`1684746387` + + 将`wts`参数添加在参数列表最后,即: + + ``` + bar=514&baz=1919810&foo=114&wts=1684746387 + ``` + +5. 计算`w_rid`并添加在其后 + + 在上一步得出的 url query 字符串后拼接第 2 步计算得出的`mixin_key`(作为盐) + + ``` + bar=514&baz=1919810&foo=114&wts=168474638772136226c6a73669787ee4fd02a74c27 + ``` + + 对这个整体进行 **md5 Hash 运算**(32-bit 字符小写),得到的值便是 Wbi Sign,也就是参数`w_rid` + + ``` + d3cbd2a2316089117134038bf4caf442 + ``` + + 最后一步,把这个计算出的值作为参数`w_rid`添加在原始参数列表后,也就完成了一次 Wbi Sign,可以调用 REST API 进行请求了 + + ``` + bar=514&baz=1919810&foo=114&wts=1684746387&w_rid=d3cbd2a2316089117134038bf4caf442 + ``` + +## Wbi签名算法实现Demo + +该 Demo 提供 [Python](#Python)、[JavaScript](#JavaScript) 语言 + +### Python + +需要`requests`依赖 + +```python +from functools import reduce +from hashlib import md5 +import urllib.parse +import time +import requests + +mixinKeyEncTab = [ + 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, + 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, + 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, + 36, 20, 34, 44, 52 +] + +def getMixinKey(orig: str): + '对 imgKey 和 subKey 进行字符顺序打乱编码' + return reduce(lambda s, i: s + orig[i], mixinKeyEncTab, '')[:32] + +def encWbi(params: dict, img_key: str, sub_key: str): + '为请求参数进行 wbi 签名' + mixin_key = getMixinKey(img_key + sub_key) + curr_time = round(time.time()) + params['wts'] = curr_time # 添加 wts 字段 + params = dict(sorted(params.items())) # 按照 key 重排参数 + # 过滤 value 中的 "!'()*" 字符 + params = { + k : ''.join(filter(lambda chr: chr not in "!'()*", str(v))) + for k, v + in params.items() + } + query = urllib.parse.urlencode(params) # 序列化参数 + wbi_sign = md5((query + mixin_key).encode()).hexdigest() # 计算 w_rid + params['w_rid'] = wbi_sign + return params + +def getWbiKeys() -> tuple[str, str]: + '获取最新的 img_key 和 sub_key' + resp = requests.get('https://api.bilibili.com/x/web-interface/nav') + resp.raise_for_status() + json_content = resp.json() + img_url: str = json_content['data']['wbi_img']['img_url'] + sub_url: str = json_content['data']['wbi_img']['sub_url'] + img_key = img_url.rsplit('/', 1)[1].split('.')[0] + sub_key = sub_url.rsplit('/', 1)[1].split('.')[0] + return img_key, sub_key + +img_key, sub_key = getWbiKeys() + +signed_params = encWbi( + params={ + 'foo': '114', + 'bar': '514', + 'baz': 1919810 + }, + img_key=img_key, + sub_key=sub_key +) +query = urllib.parse.urlencode(signed_params) +print(signed_params) +print(query) +``` + +输出内容分别是进行 Wbi 签名的后参数的 key-Value 以及 url query 形式 + +``` +{'bar': '514', 'baz': '1919810', 'foo': '114', 'wts': '1684746387', 'w_rid': 'd3cbd2a2316089117134038bf4caf442'} +bar=514&baz=1919810&foo=114&wts=1684746387&w_rid=d3cbd2a2316089117134038bf4caf442 +``` + +### JavaScript + +需要`axios`、`md5`依赖 + +```javascript +import md5 from 'md5' +import axios from 'axios' + +const mixinKeyEncTab = [ + 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, + 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, + 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, + 36, 20, 34, 44, 52 +] + +// 对 imgKey 和 subKey 进行字符顺序打乱编码 +function getMixinKey(orig) { + let temp = '' + mixinKeyEncTab.forEach((n) => { + temp += orig[n] + }) + return temp.slice(0, 32) +} + +// 为请求参数进行 wbi 签名 +function encWbi(params, img_key, sub_key) { + const mixin_key = getMixinKey(img_key + sub_key), + curr_time = Math.round(Date.now() / 1000), + chr_filter = /[!'\(\)*]/g + let query = [] + params = Object.assign(params, {wts: curr_time}) // 添加 wts 字段 + // 按照 key 重排参数 + Object.keys(params).sort().forEach((key) => { + query.push( + encodeURIComponent(key) + + '=' + + // 过滤 value 中的 "!'()*" 字符 + encodeURIComponent(('' + params[key]).replace(chr_filter, '')) + ) + }) + query = query.join('&') + const wbi_sign = md5(query + mixin_key) // 计算 w_rid + return query + '&w_rid=' + wbi_sign +} + +// 获取最新的 img_key 和 sub_key +async function getWbiKeys() { + const resp = await axios({ + url: 'https://api.bilibili.com/x/web-interface/nav', + method: 'get', + responseType: 'json' + }), + json_content = resp.data, + img_url = json_content.data.wbi_img.img_url, + sub_url = json_content.data.wbi_img.sub_url + return { + img_key: img_url.substring(img_url.lastIndexOf('/') + 1, img_url.length).split('.')[0], + sub_key: sub_url.substring(sub_url.lastIndexOf('/') + 1, sub_url.length).split('.')[0] + } +} + +const wbi_keys = await getWbiKeys() + +const query = encWbi( + { + foo: '114', + bar: '514', + baz: 1919810 + }, + wbi_keys.img_key, + wbi_keys.sub_key +) +console.log(query) +``` + +输出内容为进行 Wbi 签名的后参数的 url query 形式 + +``` +bar=514&baz=1919810&foo=114&wts=1684805578&w_rid=bb97e15f28edf445a0e4420d36f0157e +``` diff --git a/docs/other/time_stamp.md b/docs/misc/time_stamp.md similarity index 100% rename from docs/other/time_stamp.md rename to docs/misc/time_stamp.md diff --git a/docs/other/API_sign.md b/docs/other/API_sign.md deleted file mode 100644 index 7824f97..0000000 --- a/docs/other/API_sign.md +++ /dev/null @@ -1,93 +0,0 @@ -# API 签名与鉴权 - -部分客户端专用的 RESTful API 存在基于 sign 的鉴权,需要使用规定的`appkey`及其对应的`appsec`与原始请求参数进行签名计算 - -不同 `appkey` 对应不同的 app (如客户端、概念版、必剪、漫画、bililink等) - -不同平台同 app 也会存在不同的 `appkey` (如安卓端、ios端、TV端等) - -同平台同 app 下不同功能也会存在不同的 `appkey`(如登录专用、取流专用等) - -不同版本的客户端的 `appkey` 也可能不同 - -**appkey与appsec一一对应** - -## API签名的计算方式 - -首先为参数中添加`appkey`字段,然后按照参数的 key 重新排序,再将重排序后的参数使用 url query 格式序列化拼接与该 appkey 相对应的 appsec (盐值) 进行**md5 hash计算**(32位小写),该 hash 便是 API 签名 - -为参数尾部增添`sign`字段,它的值为上一步计算所得的 hash,一并作为表单提交 - -**实例:** - -使用 appkey = `1d8b6e7d45233436`, appsec = `560c52ccd288fed045859ed18bffd973` 对如下 `params` 参数进行签名 - -```python -import hashlib -import urllib.parse - -def appsign(params, appkey, appsec): - '为请求参数进行 api 签名' - params.update({'appkey': appkey}) - params = dict(sorted(params.items())) # 重排序参数 key - query = urllib.parse.urlencode(params) # 序列化参数 - sign = hashlib.md5((query+appsec).encode()).hexdigest() # 计算 api 签名 - params.update({'sign':sign}) - return params - -appkey = '1d8b6e7d45233436' -appsec = '560c52ccd288fed045859ed18bffd973' -params = { - 'id':114514, - 'str':'1919810', - 'test':'いいよ,こいよ', -} -signed_params = appsign(params, appkey, appsec) -query = urllib.parse.urlencode(signed_params) -print(signed_params) -print(query) -``` - -输出以下内容,分别是进行 api 签名后参数的 dict 以及 url query 格式 - -``` -{'appkey': '1d8b6e7d45233436', 'id': 114514, 'str': '1919810', 'test': 'いいよ,こいよ', 'sign': '01479cf20504d865519ac50f33ba3a7d'} -appkey=1d8b6e7d45233436&id=114514&str=1919810&test=%E3%81%84%E3%81%84%E3%82%88%EF%BC%8C%E3%81%93%E3%81%84%E3%82%88&sign=01479cf20504d865519ac50f33ba3a7d -``` -## 已知的APPKEY/APPSEC, 及部分参数信息 - -| APPKEY | APPSEC | platform2 | APP类型 | neuronAppId1 | mobi_app2 | 备注 | -|:--:|:--:|:--:|:--:|:--:|:--:|:--:| -| 9d5889cf67e615cd | 8fd9bb32efea8cef801fd895bef2713d | `android` | Ai4cCreatorAndroid | -| 1d8b6e7d45233436 | 560c52ccd288fed045859ed18bffd973 | `android` | 普通版(粉版) | `1` |`android`| 获取资源通用 | -| 783bbb7264451d82 | 2653583c8873dea268ab9386918b1d65 | `android` | 普通版(粉版) | `1` | `android` | 仅获取用户信息时使用(7.X及更新版本) | -| 57263273bc6b67f6 | a0488e488d1567960d3a765e8d129f90 | `android` | 普通版(粉版) | `1` |`android`| 可能来自旧版 | -| 07da50c9a0bf829f | 25bdede4e1581c836cab73a48790ca6e | `android` | 概念版(蓝版) | `3` | `android_b` | -| 191c3b6b975af184 | ******************************** | `android` | 概念版(蓝版) | `3` | `android_b` | 新出现, 仅获取用户信息时使用. 暂未知appsec | -| 178cf125136ca8ea | 34381a26236dd1171185c0beb042e1c6 | `android` | 概念版(蓝版) | `3` | `android_b` | 可能来自旧版 | -| 7d336ec01856996b | a1ce6983bc89e20a36c37f40c4f1a0dd | `android` | 概念版(蓝版) | `3` | `android_b` | 可能来自旧版 | -| dfca71928277209b | b5475a8825547a4fc26c7d518eaaa02e | `android` | HD版 | `5` | `android_hd` | -| bb3101000e232e27 | 36efcfed79309338ced0380abd824ac1 | `android` | play版(国际版) | `14` | `android_i` | -| ae57252b0c09105d | c75875c596a69eb55bd119e74b07cfe3 | `android` | play版(国际版) | `14` | `android_i` | 仅获取用户信息时使用(7.X及更新版本) | -| 8e16697a1b4f8121 | f5dd03b752426f2e623d7badb28d190a | `android` | play版(国际版) | `14` | `android_i` | 可能来自旧版 | -| 7d089525d3611b1c | acd495b248ec528c2eed1e862d393126 | `android` | 东南亚版 | `30` | `bstar_a` | -| iVGUTjsxvpLeuDCf | aHRmhWMLkdeMuILqORnYZocwMBpMEOdt | `android` | - | - | - | 视频取流专用, 仅5.X旧版使用 | -| YvirImLGlLANCLvM | JNlZNgfNGKZEpaDTkCdPQVXntXhuiJEM | `ios` | - | - | - | 视频取流专用 | -| 27eb53fc9058f8c3 | c2ed53a74eeefe3cf99fbd01d8c9c375 | `web`/`ios`? | - | - | - | 第三方授权使用 | -| 84956560bc028eb7 | 94aba54af9065f71de72f5508f1cd42e | ? | UWP版 | - | - | 部分API不接受此appkey, 返回-663错误 | -| 85eb6835b0a1034e | 2ad42749773c441109bdc0191257a664 | ? | UWP版? | - | - | 部分API不接受此appkey, 返回-663错误 | -| 4ebafd7c4951b366 | 8cb98205e9b2ad3669aad0fce12a4c13 | `ios` | iPhone客户端? | `iphone` | ? | -| 8d23902c1688a798 | 710f0212e62bd499b8d3ac6e1db9302a | `android` | AndroidBiliThings | ? | ? | -| 4c6e1021617d40d9 | e559a59044eb2701b7a8628c86aa12ae | `android` | AndroidMallTicket | ? | ? | -| c034e8b74130a886 | e4e8966b1e71847dc4a3830f2d078523 | `android` | AndroidOttSdk | `7` | ? | -| 4409e2ce8ffd12b8 | 59b43e04ad6965f34319062b478f83dd | `android` | 云视听小电视(TV版) | `9`? | `android_tv_yst`? | -| 37207f2beaebf8d7 | e988e794d4d4b6dd43bc0e89d6e90c43 | `android` | BiliLink | ? | ? | -| 9a75abf7de2d8947 | 35ca1c82be6c2c242ecc04d88c735f31 | `android` | BiliScan | ? | ? | -| aae92bc66f3edfab | af125a0d5279fd576c1b4418a3e8276d | ? | PC 投稿工具 | - | ? | -| bca7e84c2d947ac6 | 60698ba2f68e01ce44738920a0ffe768 | ? | login | - | ? | - -注释: - -1 `neuronAppId`, 产品编号,由数据平台分配,粉=1,白=2,蓝=3,直播姬=4,HD=5,海外=6,OTT=7,漫画=8,TV野版=9,小视频=10,网易漫画=11,网易漫画lite=12,网易漫画HD=13, 国际版=14. - -2 `platform`, `mobi_app` 仅供参考, 具体值需要抓包确定.