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

This commit is contained in:
SocialSisterYi
2023-05-23 09:38:21 +08:00
parent cefb7e7c40
commit 05ac3d5e2a
10 changed files with 509 additions and 173 deletions

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