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` 仅供参考, 具体值需要抓包确定.