添加文档【Wbi 接口签名】,修改目录结构
This commit is contained in:
427
docs/misc/bvid_desc.md
Normal file
427
docs/misc/bvid_desc.md
Normal 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
60
docs/misc/errcode.md
Normal 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
44
docs/misc/picture.md
Normal 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
|
||||
|
||||

|
||||
|
||||
高度限制为100
|
||||
|
||||
https://i1.hdslb.com/bfs/archive/e5fff1472bad1c0c6bcb3004205f9be23b58ffc0.jpg@100h
|
||||
|
||||

|
||||
|
||||
宽度限制为100
|
||||
|
||||
https://i1.hdslb.com/bfs/archive/e5fff1472bad1c0c6bcb3004205f9be23b58ffc0.jpg@100w
|
||||
|
||||

|
||||
|
||||
转换格式为webp
|
||||
|
||||
https://i1.hdslb.com/bfs/archive/e5fff1472bad1c0c6bcb3004205f9be23b58ffc0.jpg@.webp
|
||||
|
||||

|
||||
|
||||
转换为webp图片质量为1%
|
||||
|
||||
https://i1.hdslb.com/bfs/archive/e5fff1472bad1c0c6bcb3004205f9be23b58ffc0.jpg@1q.webp
|
||||
|
||||

|
||||
65
docs/misc/sign/APP.md
Normal file
65
docs/misc/sign/APP.md
Normal 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
54
docs/misc/sign/APPKey.md
Normal 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
250
docs/misc/sign/wbi.md
Normal 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
73
docs/misc/time_stamp.md
Normal 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>
|
||||
Reference in New Issue
Block a user