添加文档【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

427
docs/misc/bvid_desc.md Normal file
View File

@@ -0,0 +1,427 @@
# bvid说明
2020-03-23 B站推出了全新的稿件视频id`bvid`来接替之前的`avid`,其意义与之相同
详见:
1. [【升级公告】AV号全面升级至BV号专栏](https://www.bilibili.com/read/cv5167957)
2. [【升级公告】AV号全面升级至BV号](https://www.bilibili.com/blackboard/activity-BV-PC.html)
## 概述
### 格式
“bvid”恒为长度为 12 的字符串前两个字母为大写“BV”后 10 个为 base58 计算结果
### 实质
“bvid"为“avid”的base58编码可通过算法进行相互转化
### avid发号方式的变化
从 2009-09-09 09:09:09 [av2](https://www.bilibili.com/video/av2) 的发布到 2020-03-28 19:45:02 [av99999999](https://www.bilibili.com/video/av99999999) 的发布B站结束了以投稿时间为顺序的avid发放改为随机发放avid
~~暗示B站东方要完泪目~~
## 算法概述
~~算法以及程序主要参考[知乎@mcfx的回答](https://www.zhihu.com/question/381784377/answer/1099438784)~~
实际上该算法并不完整,新的算法参考自[【揭秘】av号转bv号的过程](https://www.bilibili.com/video/BV1N741127Tj)
### av->bv算法
注:本算法及示例程序仅能编解码`avid < 29460791296`,且暂无法验证`avid >= 29460791296`的正确性
再注:本人不清楚新算法能否编解码`avid >= 29460791296`
1. a = (avid ⊕ 177451812) + 100618342136696320
2. 以 i 为循环变量循环 6 次 b[i] = (a / 58 ^ i) % 58
3. 将 b[i] 中各个数字转换为以下码表中的字符
码表:
> fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF
4. 初始化字符串 b[i]=` `
5. 按照以下字符顺序编码表编码并填充至 b[i]
字符顺序编码表:
> 0 -> 9
>
> 1 -> 8
>
> 2 -> 1
>
> 3 -> 6
>
> 4 -> 2
>
> 5 -> 4
>
> 6 -> 0
>
> 7 -> 7
>
> 8 -> 3
>
> 9 -> 5
### bv->av算法
为以上算法的逆运算
## 编程实现
使用 [Python](#Python) [C](#C) [TypeScript](#TypeScript) [Java](#Java) [Kotlin](#Kotlin) [Golang](#Golang) [Rust](#Rust) 等语言作为示例,欢迎社区提交更多例程
注: 新算法只提供了 [Python](#Python) 和 [Rust](#Rust) 版本
### Python
```python
XOR = 177451812
ADD = 100618342136696320
TABLE = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF"
MAP = {
0:9,
1:8,
2:1,
3:6,
4:2,
5:4,
6:0,
7:7,
8:3,
9:5
}
def av2bv(av):
av = (av ^ XOR) + ADD
bv = [""]*10
for i in range(10):
bv[MAP[i]] = TABLE[(av//58**i)%58]
return "".join(bv)
def bv2av(bv):
av = [""]*10
s = 0
for i in range(10):
s += TABLE.find(bv[MAP[i]])*58**i
av=(s-ADD)^XOR
return(av)
def main():
while True:
mod = input("1.AV2BV\n2.BV2AV\n3.Exit\n你的选择:")
if mod == "1":
print("BV号是: BV"+av2bv(int(input("AV号是:"))))
elif mod == "2":
print("AV号是 AV"+str(bv2av(input("BV号是"))))
elif mod == "3":
break
else:
print("输入错误请重新输入")
main()
```
### C
```c
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
const char table[] = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF"; // 码表
char tr[124]; // 反查码表
const unsigned long long XOR = 177451812; // 固定异或值
const unsigned long long ADD = 8728348608; // 固定加法值
const int s[] = {11, 10, 3, 8, 4, 6}; // 位置编码表
// 初始化反查码表
void tr_init() {
for (int i = 0; i < 58; i++)
tr[table[i]] = i;
}
unsigned long long bv2av(char bv[]) {
unsigned long long r = 0;
unsigned long long av;
for (int i = 0; i < 6; i++)
r += tr[bv[s[i]]] * (unsigned long long)pow(58, i);
av = (r - ADD) ^ XOR;
return av;
}
char *av2bv(unsigned long long av) {
char *result = (char*)malloc(13);
strcpy(result,"BV1 4 1 7 ");
av = (av ^ XOR) + ADD;
for (int i = 0; i < 6; i++)
result[s[i]] = table[(unsigned long long)(av / (unsigned long long)pow(58, i)) % 58];
return result;
}
int main() {
tr_init();
printf("%s\n", av2bv(170001));
printf("%u\n", bv2av("BV17x411w7KC"));
return 0;
}
```
输出为:
```
BV17x411w7KC
170001
```
### TypeScript
感谢[#417](https://github.com/SocialSisterYi/bilibili-API-collect/issues/417#issuecomment-1186475063)提供
```typescript
export default class BvCode {
private TABEL = 'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF'; // 码表
private TR: Record<string, number> = {}; // 反查码表
private S = [11, 10, 3, 8, 4, 6]; // 位置编码表
private XOR = 177451812; // 固定异或值
private ADD = 8728348608; // 固定加法值
constructor() {
// 初始化反查码表
const len = this.TABEL.length;
for (let i = 0; i < len; i++) {
this.TR[this.TABEL[i]] = i;
}
}
av2bv(av: number): string {
const x_ = (av ^ this.XOR) + this.ADD;
const r = ['B', 'V', '1', , , '4', , '1', , '7'];
for (let i = 0; i < 6; i++) {
r[this.S[i]] = this.TABEL[Math.floor(x_ / 58 ** i) % 58];
}
return r.join('');
}
bv2av(bv: string): number {
let r = 0;
for (let i = 0; i < 6; i++) {
r += this.TR[bv[this.S[i]]] * 58 ** i;
}
return (r - this.ADD) ^ this.XOR;
}
}
const bvcode = new BvCode();
console.log(bvcode.av2bv(170001));
console.log(bvcode.bv2av('BV17x411w7KC'));
```
输出为:
```
BV17x411w7KC
170001
```
### Java
```java
/**
* 算法来自https://www.zhihu.com/question/381784377/answer/1099438784
*/
public class Util {
private static final String TABLE = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF";
private static final int[] S = new int[]{11, 10, 3, 8, 4, 6};
private static final int XOR = 177451812;
private static final long ADD = 8728348608L;
private static final Map<Character, Integer> MAP = new HashMap<>();
static {
for (int i = 0; i < 58; i++) {
MAP.put(TABLE.charAt(i), i);
}
}
public static String aidToBvid(int aid) {
long x = (aid ^ XOR) + ADD;
char[] chars = new char[]{'B', 'V', '1', ' ', ' ', '4', ' ', '1', ' ', '7', ' ', ' '};
for (int i = 0; i < 6; i++) {
int pow = (int) Math.pow(58, i);
long i1 = x / pow;
int index = (int) (i1 % 58);
chars[S[i]] = TABLE.charAt(index);
}
return String.valueOf(chars);
}
public static int bvidToAid(String bvid) {
long r = 0;
for (int i = 0; i < 6; i++) {
r += MAP.get(bvid.charAt(S[i])) * Math.pow(58, i);
}
return (int) ((r - ADD) ^ XOR);
}
}
```
### Kotlin
```kotlin
/**
* 此程序非完全原创改编自GH站内某大佬的Java程序修改了部分代码且转换为Kotlin
* 算法来源同上
*/
object VideoUtils {
//这里是由知乎大佬不知道用什么方法得出的转换用数字
var ss = intArrayOf(11, 10, 3, 8, 4, 6, 2, 9, 5, 7)
var xor: Long = 177451812 //二进制时加减数1
var add = 8728348608L //十进制时加减数2
//变量初始化工作,加载哈希表
private const val table = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF"
private val mp = HashMap<String, Int>()
private val mp2 = HashMap<Int, String>()
//现在定义av号和bv号互转的方法
//定义一个power乘方方法这是转换进制必要的
fun power(a: Int, b: Int): Long {
var power: Long = 1
for (c in 0 until b) power *= a.toLong()
return power
}
//bv转av方法
fun bv2av(s: String): String {
var r: Long = 0
//58进制转换
for (i in 0..57) {
val s1 = table.substring(i, i + 1)
mp[s1] = i
}
for (i in 0..5) {
r += mp[s.substring(ss[i], ss[i] + 1)]!! * power(58, i)
}
//转换完成后,需要处理,带上两个随机数
return (r - add xor xor).toString()
}
//av转bv方法
fun av2bv(st: String): String {
try {
var s = java.lang.Long.valueOf(st.split("av".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()[1])
val sb = StringBuffer("BV1 4 1 7 ")
//逆向思路,先将随机数还原
s = (s xor xor) + add
//58进制转回
for (i in 0..57) {
val s1 = table.substring(i, i + 1)
mp2[i] = s1
}
for (i in 0..5) {
val r = mp2[(s / power(58, i) % 58).toInt()]
sb.replace(ss[i], ss[i] + 1, r!!)
}
return sb.toString()
} catch (e: ArrayIndexOutOfBoundsException) {
return ""
}
}
}
```
### Golang
```go
package main
import "math"
const TABLE = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF"
var S = [11]uint{11, 10, 3, 8, 4, 6}
const XOR = 177451812
const ADD = 8728348608
var TR = map[string]int64{}
// 初始化 TR
func init() {
for i := 0; i < 58; i++ {
TR[TABLE[i:i+1]] = int64(i)
}
}
func BV2AV(bv string) int64 {
r := int64(0)
for i := 0; i < 6; i++ {
r += TR[bv[S[i]:S[i]+1]] * int64(math.Pow(58, float64(i)))
}
return (r - ADD) ^ XOR
}
func AV2BV(av int64) string {
x := (av ^ XOR) + ADD
r := []rune("BV1 4 1 7 ")
for i := 0; i < 6; i++ {
r[S[i]] = rune(TABLE[x/int64(math.Pow(58, float64(i)))%58])
}
return string(r)
}
func main() {
println(AV2BV(170001))
println(BV2AV("BV17x411w7KC"))
}
```
输出为:
```
BV17x411w7KC
170001
```
### Rust
crate: https://github.com/stackinspector/bvid
```rust
// Copyright (c) 2023 stackinspector. MIT license.
const XORN: u64 = 177451812;
const ADDN: u64 = 100618342136696320;
const TABLE: [u8; 58] = *b"fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF";
const MAP: [usize; 10] = [9, 8, 1, 6, 2, 4, 0, 7, 3, 5];
const REV_TABLE: [u8; 74] = [
13, 12, 46, 31, 43, 18, 40, 28, 5, 0, 0, 0, 0, 0, 0, 0, 54, 20, 15, 8,
39, 57, 45, 36, 0, 38, 51, 42, 49, 52, 0, 53, 7, 4, 9, 50, 10, 44, 34, 6,
25, 1, 0, 0, 0, 0, 0, 0, 26, 29, 56, 3, 24, 0, 47, 27, 22, 41, 16, 0,
11, 37, 2, 35, 21, 17, 33, 30, 48, 23, 55, 32, 14, 19,
];
const POW58: [u64; 10] = [
1, 58, 3364, 195112, 11316496, 656356768, 38068692544,
2207984167552, 128063081718016, 7427658739644928,
];
fn av2bv(avid: u64) -> [u8; 10] {
let a = (avid ^ XORN) + ADDN;
let mut bvid = [0; 10];
for i in 0..10 {
bvid[MAP[i]] = TABLE[(a / POW58[i]) as usize % 58];
}
bvid
}
fn bv2av(bvid: [u8; 10]) -> u64 {
let mut a = 0;
for i in 0..10 {
a += REV_TABLE[bvid[MAP[i]] as usize - 49] as u64 * POW58[i];
}
(a - ADDN) ^ XORN
}
// assert_eq!(*b"17x411w7KC", av2bv(170001));
// assert_eq!(170001, bv2av(*b"17x411w7KC"));
```

60
docs/misc/errcode.md Normal file
View File

@@ -0,0 +1,60 @@
# 公共错误码
下表为大部分接口返回值中`code`字段值中公共的错误代码
## 权限类
| 代码 | 含义 |
| ---- | -------------------------- |
| -1 | 应用程序不存在或已被封禁 |
| -2 | Access Key 错误 |
| -3 | API 校验密匙错误 |
| -4 | 调用方对该 Method 没有权限 |
| -101 | 账号未登录 |
| -102 | 账号被封停 |
| -103 | 积分不足 |
| -104 | 硬币不足 |
| -105 | 验证码错误 |
| -106 | 账号非正式会员或在适应期 |
| -107 | 应用不存在或者被封禁 |
| -108 | 未绑定手机 |
| -110 | 未绑定手机 |
| -111 | csrf 校验失败 |
| -112 | 系统升级中 |
| -113 | 账号尚未实名认证 |
| -114 | 请先绑定手机 |
| -115 | 请先完成实名认证 |
## 请求类
| 代码 | 含义 |
| ---- | --------------------- |
| -304 | 木有改动 |
| -307 | 撞车跳转 |
| -400 | 请求错误 |
| -401 | 未认证 (或非法请求) |
| -403 | 访问权限不足 |
| -404 | 啥都木有 |
| -405 | 不支持该方法 |
| -409 | 冲突 |
| -412 | 请求被拦截 (客户端 ip 被服务端风控) |
| -500 | 服务器错误 |
| -503 | 过载保护,服务暂不可用 |
| -504 | 服务调用超时 |
| -509 | 超出限制 |
| -616 | 上传文件不存在 |
| -617 | 上传文件太大 |
| -625 | 登录失败次数太多 |
| -626 | 用户不存在 |
| -628 | 密码太弱 |
| -629 | 用户名或密码错误 |
| -632 | 操作对象数量限制 |
| -643 | 被锁定 |
| -650 | 用户等级太低 |
| -652 | 重复的用户 |
| -658 | Token 过期 |
| -662 | 密码时间戳过期 |
| -688 | 地理区域限制 |
| -689 | 版权限制 |
| -701 | 扣节操失败 |
|-8888|对不起,服务器开小差了~ (ಥ﹏ಥ)|

44
docs/misc/picture.md Normal file
View File

@@ -0,0 +1,44 @@
# 图片格式化
对于\*.hdslb.com/bfs下的图片文件都可以使用以下格式化参数
> \*.hdslb.com/bfs/\*/\*.\[jpg/png/gif\]@{width}w\_{high}h\_{quality}q.{format}
| 可选参数 | 含义 | 备注 |
| -------- | ---------------- | ---------------- |
| width | 图片最大限制宽度 | |
| high | 图片最大限制高度 | |
| quality | 图片质量百分比 | 仅限webp |
| format | 图片格式 | 仅限png/jpg/webp |
**示例:**
原始图片
https://i1.hdslb.com/bfs/archive/e5fff1472bad1c0c6bcb3004205f9be23b58ffc0.jpg
![](https://i1.hdslb.com/bfs/archive/e5fff1472bad1c0c6bcb3004205f9be23b58ffc0.jpg)
高度限制为100
https://i1.hdslb.com/bfs/archive/e5fff1472bad1c0c6bcb3004205f9be23b58ffc0.jpg@100h
![](https://i1.hdslb.com/bfs/archive/e5fff1472bad1c0c6bcb3004205f9be23b58ffc0.jpg@100h)
宽度限制为100
https://i1.hdslb.com/bfs/archive/e5fff1472bad1c0c6bcb3004205f9be23b58ffc0.jpg@100w
![](https://i1.hdslb.com/bfs/archive/e5fff1472bad1c0c6bcb3004205f9be23b58ffc0.jpg@100w)
转换格式为webp
https://i1.hdslb.com/bfs/archive/e5fff1472bad1c0c6bcb3004205f9be23b58ffc0.jpg@.webp
![](https://i1.hdslb.com/bfs/archive/e5fff1472bad1c0c6bcb3004205f9be23b58ffc0.jpg@.webp)
转换为webp图片质量为1%
https://i1.hdslb.com/bfs/archive/e5fff1472bad1c0c6bcb3004205f9be23b58ffc0.jpg@1q.webp
![](https://i1.hdslb.com/bfs/archive/e5fff1472bad1c0c6bcb3004205f9be23b58ffc0.jpg@1q.webp)

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

73
docs/misc/time_stamp.md Normal file
View File

@@ -0,0 +1,73 @@
# 获取当前时间戳
## 获取当前时间戳
> https://api.bilibili.com/x/report/click/now
*请求方式GET*
**json回复**
根对象:
| 字段 | 类型 | 内容 | 备注 |
| ------- | ---- | -------- | ------- |
| code | num | 返回值 | 0成功 |
| message | str | 错误信息 | 默认为0 |
| ttl | num | 1 | |
| data | obj | 信息本体 | |
`data`对象:
| 字段 | 类型 | 内容 | 备注 |
| ---- | ---- | ------------ | ---- |
| now | num | 当前的时间戳 | |
**示例:**
```shell
curl 'https://api.bilibili.com/x/report/click/now'
```
<details>
<summary>查看响应示例:</summary>
```json
{
"code": 0,
"message": "0",
"ttl": 1,
"data": {
"now": 1592666471
}
}
```
</details>
## 获取服务器端UTC时间
> https://interface.bilibili.com/serverdate.js
*请求方式GET*
**js回复**
```js
window.serverdate = Date.UTC(YYYY, M, D, h, m, s);
```
**示例:**
```shell
curl 'https://interface.bilibili.com/serverdate.js'
```
<details>
<summary>查看响应示例:</summary>
```js
window.serverdate = Date.UTC(2021, 4, 16, 17, 31, 8);
```
</details>