添加文档【Wbi 接口签名】,修改目录结构

This commit is contained in:
SocialSisterYi 2023-05-23 09:38:21 +08:00
parent cefb7e7c40
commit 05ac3d5e2a
No known key found for this signature in database
GPG Key ID: 4359DB0F41BEEBBA
10 changed files with 509 additions and 173 deletions

View File

@ -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)

View File

@ -27,30 +27,33 @@
| -------------------- | ---- | ---------------- | ------------------------------------------------- |
| isLogin | bool | 是否已登录 | false未登录<br />true已登录 |
| email_verified | num | 是否验证邮箱地址 | 0未验证<br />1已验证 |
| face | str | 用户头像url | |
| face | str | 用户头像 url | |
| level_info | obj | 等级信息 | |
| mid | num | 用户mid | |
| mid | num | 用户 mid | |
| mobile_verified | num | 是否验证手机号 | 0未验证<br />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<br />1有 |
| vipType | num | 会员类型 | 0<br />1月度大会员<br />2年度及以上大会员 |
| vip_pay_type | num | 会员开通状态 | 0<br />1有 |
| vip_theme_type | num | 0 | 作用尚不明确 |
| vip_theme_type | num | | |
| vip_label | obj | 会员标签 | |
| vip_avatar_subscript | num | 是否显示会员图标 | 0不显示<br />1显示 |
| vip_nickname_color | str | 会员昵称颜色 | 颜色码 |
| wallet | obj | B币钱包信息 | |
| has_shop | bool | 是否拥有推广商品 | false<br />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非硬核会员<br />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大会员<br />annual_vip年度大会员<br />ten_annual_vip十年大会员<br />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'
```
<details>
@ -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
}
}
```

65
docs/misc/sign/APP.md Normal file
View File

@ -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
```

54
docs/misc/sign/APPKey.md Normal file
View File

@ -0,0 +1,54 @@
# APIKey
以下为已知的 APPkey / APPSec及部分使用场景参数信息均来自抓包与逆向工程
| APPKEY | APPSEC | platform<sup>2</sup> | APP类型 | neuronAppId<sup>1</sup> | mobi_app<sup>2</sup> | 备注 |
| :--------------: | :------------------------------: | :------------------: | :----------------: | :---------------------: | :------------------: | :----------------------------------------: |
| 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 | - | ? | |
注释:
<sup>1</sup> `neuronAppId`,产品编号,由数据平台分配,详情如下:
- 粉(国内版)=1
- 白GooglePlay 版)=2
- 蓝(东南亚版)=3
- 直播姬=4
- HD=5
- 海外=6
- OTT=7
- 漫画=8
- TV野版=9
- 小视频=10
- 网易漫画=11
- 网易漫画lite=12
- 网易漫画HD=13,
- 国际版=14
<sup>2</sup> `platform`, `mobi_app` 仅供参考, 具体值需要抓包确定.

250
docs/misc/sign/wbi.md Normal file
View File

@ -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
```

View File

@ -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 | platform<sup>2</sup> | APP类型 | neuronAppId<sup>1</sup> | mobi_app<sup>2</sup> | 备注 |
|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
| 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 | - | ? |
注释:
<sup>1</sup> `neuronAppId`, 产品编号,由数据平台分配,粉=1白=2蓝=3直播姬=4HD=5海外=6OTT=7漫画=8TV野版=9小视频=10网易漫画=11网易漫画lite=12网易漫画HD=13, 国际版=14.
<sup>2</sup> `platform`, `mobi_app` 仅供参考, 具体值需要抓包确定.