2023-12-11 18:33:33 +08:00
# WBI 签名
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
自 2023 年 3 月起, Bilibili Web 端部分接口开始采用 WBI 签名鉴权,表现在 REST API 请求时在 Query param 中添加了 `w_rid` 和 `wts` 字段。WBI 签名鉴权独立于 [APP 鉴权 ](APP.md ) 与其他 Cookie 鉴权,目前被认为是一种 Web 端风控手段。
2023-05-23 09:38:21 +08:00
2024-08-21 19:02:39 +08:00
经持续观察,大部分查询性接口都已经或准备采用 WBI 签名鉴权,请求 WBI 签名鉴权接口时,若签名参数 `w_rid` 与时间戳 `wts` 缺失、错误,会返回 `v_voucher` ,如:
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
```json
{"code":0,"message":"0","ttl":1,"data":{"v_voucher":"voucher_******"}}
```
感谢 [#631 ](https://github.com/SocialSisterYi/bilibili-API-collect/issues/631 ) 的研究与逆向工程。
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
细节更新:[#885](https://github.com/SocialSisterYi/bilibili-API-collect/issues/885)。
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
## WBI 签名算法
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
1. 获取实时口令 `img_key` 、`sub_key`
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
从 [nav 接口 ](../../login/login_info.md#导航栏用户信息 ) 中获取 `img_url` 、`sub_url` 两个字段的参数。
2024-08-21 19:02:39 +08:00
或从 [bili_ticket 接口 ](bili_ticket.md#接口 ) 中获取 `img` `sub` 两个字段的参数。
2023-12-11 18:33:33 +08:00
**注:`img_url`、`sub_url` 两个字段的值看似为存于 BFS 中的 png 图片 url, 实则只是经过伪装的实时 Token, 故无需且不能试图访问这两个 url**
2023-05-23 09:38:21 +08:00
```json
2023-12-11 18:33:33 +08:00
{"code":-101,"message":"账号未登录","ttl":1,"data":{"isLogin":false,"wbi_img":{"img_url":"https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png","sub_url":"https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png"}}}
2023-05-23 09:38:21 +08:00
```
2023-12-11 18:33:33 +08:00
截取其文件名,分别记为 `img_key` 、`sub_key`,如上述例子中的 `7cd084941338484aae1ad9425b84077c` 和 `4932caff0ff746eab6f01bf08b70ac45` 。
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
`img_key` 、`sub_key` 全站统一使用,观测知应为**每日更替**,使用时建议做好**缓存和刷新**处理。
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
特别地,发现部分接口将 `img_key` 、`sub_key` 硬编码进 JavaScript 文件内,如搜索接口 `https://s1.hdslb.com/bfs/static/laputa-search/client/assets/index.1ea39bea.js` ,暂不清楚原因及影响。
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
2. 打乱重排实时口令获得 `mixin_key`
把上一步获取到的 `sub_key` 拼接在 `img_key` 后面(下例记为 `raw_wbi_key` ),遍历重排映射表 `MIXIN_KEY_ENC_TAB` ,取出 `raw_wbi_key` 中对应位置的字符拼接得到新的字符串,截取前 32 位,即为 `mixin_key` 。
重排映射表 `MIXIN_KEY_ENC_TAB` 长为 64, 内容如下:
```rust
const MIXIN_KEY_ENC_TAB: [u8; 64] = [
2023-05-23 09:38:21 +08:00
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
]
```
2023-12-11 18:33:33 +08:00
重排操作如下例:
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
```rust
fn gen_mixin_key(raw_wbi_key: impl AsRef< [u8]>) -> String {
const MIXIN_KEY_ENC_TAB: [u8; 64] = [
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,
];
let raw_wbi_key = raw_wbi_key.as_ref();
let mut mixin_key = {
let binding = MIXIN_KEY_ENC_TAB
.iter()
// 此步操作即遍历 MIXIN_KEY_ENC_TAB, 取出 raw_wbi_key 中对应位置的字符
.map(|n| raw_wbi_key[*n as usize])
// 并收集进数组内
.collect::< Vec < u8 > >();
unsafe { String::from_utf8_unchecked(binding) }
};
let _ = mixin_key.split_off(32); // 截取前 32 位字符
mixin_key
}
2023-05-23 09:38:21 +08:00
```
2023-12-11 18:33:33 +08:00
如 `img_key` -> `7cd084941338484aae1ad9425b84077c` 、`sub_key` -> `4932caff0ff746eab6f01bf08b70ac45` 经过上述操作后得到 `mixin_key` -> `ea1db124af3c7062474693fa704f4ff8` 。
3. 计算签名(即 `w_rid` )
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
若下方内容为欲签名的**原始**请求参数(以 JavaScript Object 为例)
2023-05-23 09:38:21 +08:00
```javascript
{
2023-06-21 08:39:47 +08:00
foo: '114',
bar: '514',
2023-06-23 13:49:03 +08:00
zab: 1919810
2023-05-23 09:38:21 +08:00
}
```
2023-12-11 18:33:33 +08:00
`wts` 字段的值应为当前以秒为单位的 Unix 时间戳,如 `1702204169`
2023-06-23 13:49:03 +08:00
2023-12-11 18:33:33 +08:00
复制一份参数列表,添加 `wts` 参数,即:
2023-05-23 09:38:21 +08:00
2023-06-23 13:49:03 +08:00
```javascript
{
foo: '114',
bar: '514',
zab: 1919810,
2023-12-11 18:33:33 +08:00
wts: 1702204169
2023-06-23 13:49:03 +08:00
}
2023-05-23 09:38:21 +08:00
```
2023-06-23 13:49:03 +08:00
2023-12-11 18:33:33 +08:00
随后按键名升序排序后编码 URL Query, 拼接前面得到的 `mixin_key` ,如 `bar=514&foo=114&wts=1702204169&zab=1919810ea1db124af3c7062474693fa704f4ff8` ,计算其 MD5 即为 `w_rid` 。
2023-06-23 13:49:03 +08:00
2023-12-11 18:33:33 +08:00
需要注意的是:如果参数值含中文或特殊字符等,编码字符字母应当**大写** (部分库会编码为小写字母),空格应当编码为 `%20` (部分库按 `application/x-www-form-urlencoded` 约定编码为 `+` )。
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
例如:
2023-06-21 08:39:47 +08:00
```javascript
{
foo: 'one one four',
bar: '五一四',
baz: 1919810
}
```
2023-12-11 18:33:33 +08:00
应该被编码为 `bar=%E4%BA%94%E4%B8%80%E5%9B%9B&baz=1919810&foo=one%20one%20four` 。
2023-06-21 08:39:47 +08:00
2023-12-11 18:33:33 +08:00
4. 向原始请求参数中添加 `w_rid` 、`wts` 字段
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
将上一步得到的 `w_rid` 以及前面的 `wts` 追加到**原始**请求参数编码得到的 URL Query 后即可,目前看来无需对原始请求参数排序。
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
如前例最终得到 `bar=514&foo=114&zab=1919810&w_rid=8f6f2b5b3d485fe1886cec6a0be8c5d4&wts=1702204169` 。
2023-05-23 09:38:21 +08:00
2023-12-11 18:33:33 +08:00
## Demo
2023-05-23 09:38:21 +08:00
2024-07-29 10:03:09 +08:00
含 [Python ](#Python )、[JavaScript](#JavaScript)、[Golang](#Golang)、[C#](#CSharp)、[Java](#Java)、[Kotlin](#Kotlin)、[Swift](#Swift)、[C++](#CPlusPlus)、[Rust](#Rust) 语言编写的 Demo 。
2023-05-23 09:38:21 +08:00
### 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'
2024-06-13 10:30:14 +08:00
headers = {
2024-04-02 23:21:40 +08:00
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
'Referer': 'https://www.bilibili.com/'
}
resp = requests.get('https://api.bilibili.com/x/web-interface/nav', headers=headers)
2023-05-23 09:38:21 +08:00
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 形式
```
2023-12-11 18:33:33 +08:00
{'bar': '514', 'baz': '1919810', 'foo': '114', 'wts': '1702204169', 'w_rid': 'd3cbd2a2316089117134038bf4caf442'}
bar=514& baz=1919810& foo=114& wts=1702204169& w_rid=d3cbd2a2316089117134038bf4caf442
2023-05-23 09:38:21 +08:00
```
### JavaScript
2024-06-13 10:32:00 +08:00
需要 `fetch` (浏览器、NodeJS等环境自带)、`md5` 依赖
< CodeGroup >
< CodeGroupItem title = "JavaScript" >
2023-05-23 09:38:21 +08:00
```javascript
import md5 from 'md5'
const mixinKeyEncTab = [
2023-12-03 23:01:43 +08:00
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
2023-05-23 09:38:21 +08:00
]
// 对 imgKey 和 subKey 进行字符顺序打乱编码
2023-12-03 23:01:43 +08:00
const getMixinKey = (orig) => mixinKeyEncTab.map(n => orig[n]).join('').slice(0, 32)
2023-05-23 09:38:21 +08:00
// 为请求参数进行 wbi 签名
function encWbi(params, img_key, sub_key) {
2023-12-03 23:01:43 +08:00
const mixin_key = getMixinKey(img_key + sub_key),
curr_time = Math.round(Date.now() / 1000),
chr_filter = /[!'()*]/g
Object.assign(params, { wts: curr_time }) // 添加 wts 字段
// 按照 key 重排参数
const query = Object
.keys(params)
.sort()
.map(key => {
// 过滤 value 中的 "!'()*" 字符
const value = params[key].toString().replace(chr_filter, '')
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
2023-05-23 09:38:21 +08:00
})
2023-12-03 23:01:43 +08:00
.join('& ')
const wbi_sign = md5(query + mixin_key) // 计算 w_rid
return query + '& w_rid=' + wbi_sign
2023-05-23 09:38:21 +08:00
}
// 获取最新的 img_key 和 sub_key
async function getWbiKeys() {
2023-12-03 23:01:43 +08:00
const res = await fetch('https://api.bilibili.com/x/web-interface/nav', {
headers: {
// SESSDATA 字段
2024-06-13 10:32:00 +08:00
Cookie: 'SESSDATA=xxxxxx',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
2024-04-02 23:21:40 +08:00
Referer: 'https://www.bilibili.com/'//对于直接浏览器调用可能不适用
2023-05-23 09:38:21 +08:00
}
2023-12-03 23:01:43 +08:00
})
const { data: { wbi_img: { img_url, sub_url } } } = await res.json()
return {
img_key: img_url.slice(
img_url.lastIndexOf('/') + 1,
img_url.lastIndexOf('.')
),
sub_key: sub_url.slice(
sub_url.lastIndexOf('/') + 1,
sub_url.lastIndexOf('.')
)
}
2023-05-23 09:38:21 +08:00
}
2023-12-03 23:01:43 +08:00
async function main() {
const web_keys = await getWbiKeys()
const params = { foo: '114', bar: '514', baz: 1919810 },
img_key = web_keys.img_key,
sub_key = web_keys.sub_key
const query = encWbi(params, img_key, sub_key)
console.log(query)
}
main()
2023-05-23 09:38:21 +08:00
```
2024-06-13 10:32:00 +08:00
< / CodeGroupItem >
< CodeGroupItem title = "TypeScript" >
```typescript
import md5 from 'md5'
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 进行字符顺序打乱编码
const getMixinKey = (orig: string) =>
mixinKeyEncTab
.map((n) => orig[n])
.join("")
.slice(0, 32);
// 为请求参数进行 wbi 签名
function encWbi(
params: { [key: string]: string | number | object },
img_key: string,
sub_key: string
) {
const mixin_key = getMixinKey(img_key + sub_key),
curr_time = Math.round(Date.now() / 1000),
chr_filter = /[!'()*]/g;
Object.assign(params, { wts: curr_time }); // 添加 wts 字段
// 按照 key 重排参数
const query = Object.keys(params)
.sort()
.map((key) => {
// 过滤 value 中的 "!'()*" 字符
const value = params[key].toString().replace(chr_filter, "");
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}` ;
})
.join("&");
const wbi_sign = md5(query + mixin_key); // 计算 w_rid
return query + "& w_rid=" + wbi_sign;
}
// 获取最新的 img_key 和 sub_key
async function getWbiKeys(SESSDATA: string) {
const res = await fetch('https://api.bilibili.com/x/web-interface/nav', {
headers: {
// SESSDATA 字段
Cookie: `SESSDATA=${SESSDATA}` ,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
Referer: 'https://www.bilibili.com/'//对于直接浏览器调用可能不适用
}
})
const {
data: {
wbi_img: { img_url, sub_url },
},
} = (await res.json()) as {
data: {
wbi_img: { img_url: string; sub_url: string };
};
};
return {
img_key: img_url.slice(
img_url.lastIndexOf('/') + 1,
img_url.lastIndexOf('.')
),
sub_key: sub_url.slice(
sub_url.lastIndexOf('/') + 1,
sub_url.lastIndexOf('.')
)
}
}
async function main() {
const web_keys = await getWbiKeys("SESSDATA的值")
const params = { foo: '114', bar: '514', baz: 1919810 },
img_key = web_keys.img_key,
sub_key = web_keys.sub_key
const query = encWbi(params, img_key, sub_key)
console.log(query)
}
main()
```
< / CodeGroupItem >
< / CodeGroup >
2023-05-23 09:38:21 +08:00
输出内容为进行 Wbi 签名的后参数的 url query 形式
```
bar=514& baz=1919810& foo=114& wts=1684805578& w_rid=bb97e15f28edf445a0e4420d36f0157e
```
2023-06-04 17:22:25 +08:00
### Golang
需要 `github.com/tidwall/gjson` 作为依赖
2024-09-19 23:17:33 +08:00
```go
2023-06-04 17:22:25 +08:00
package main
import (
2024-04-02 23:21:40 +08:00
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/tidwall/gjson"
2023-06-04 17:22:25 +08:00
)
2023-08-11 10:00:36 +08:00
var (
2024-04-02 23:21:40 +08:00
mixinKeyEncTab = []int{
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,
}
cache sync.Map
lastUpdateTime time.Time
2023-08-11 10:00:36 +08:00
)
2023-06-04 17:22:25 +08:00
2023-08-11 10:00:36 +08:00
func main() {
2024-04-02 23:21:40 +08:00
urlStr := "https://api.bilibili.com/x/space/wbi/acc/info?mid=1850091"
newUrlStr, err := signAndGenerateURL(urlStr)
if err != nil {
fmt.Printf("Error: %s", err)
return
}
req, err := http.NewRequest("GET", newUrlStr, nil)
if err != nil {
fmt.Printf("Error: %s", err)
return
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Referer", "https://www.bilibili.com/")
response, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("Request failed: %s", err)
return
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
fmt.Printf("Failed to read response: %s", err)
return
}
fmt.Println(string(body))
2023-06-04 17:22:25 +08:00
}
2023-08-11 10:00:36 +08:00
func signAndGenerateURL(urlStr string) (string, error) {
2024-04-02 23:21:40 +08:00
urlObj, err := url.Parse(urlStr)
if err != nil {
return "", err
}
imgKey, subKey := getWbiKeysCached()
query := urlObj.Query()
params := map[string]string{}
for k, v := range query {
params[k] = v[0]
}
newParams := encWbi(params, imgKey, subKey)
for k, v := range newParams {
query.Set(k, v)
}
urlObj.RawQuery = query.Encode()
newUrlStr := urlObj.String()
return newUrlStr, nil
2023-06-04 17:22:25 +08:00
}
2023-08-11 10:00:36 +08:00
func encWbi(params map[string]string, imgKey, subKey string) map[string]string {
2024-04-02 23:21:40 +08:00
mixinKey := getMixinKey(imgKey + subKey)
currTime := strconv.FormatInt(time.Now().Unix(), 10)
params["wts"] = currTime
// Sort keys
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
// Remove unwanted characters
for k, v := range params {
v = sanitizeString(v)
params[k] = v
}
// Build URL parameters
query := url.Values{}
for _, k := range keys {
query.Set(k, params[k])
}
queryStr := query.Encode()
// Calculate w_rid
hash := md5.Sum([]byte(queryStr + mixinKey))
params["w_rid"] = hex.EncodeToString(hash[:])
return params
2023-08-11 10:00:36 +08:00
}
2023-06-04 17:22:25 +08:00
2023-08-11 10:00:36 +08:00
func getMixinKey(orig string) string {
2024-04-02 23:21:40 +08:00
var str strings.Builder
for _, v := range mixinKeyEncTab {
if v < len ( orig ) {
str.WriteByte(orig[v])
}
}
return str.String()[:32]
2023-06-04 17:22:25 +08:00
}
2023-08-11 10:00:36 +08:00
func sanitizeString(s string) string {
2024-04-02 23:21:40 +08:00
unwantedChars := []string{"!", "'", "(", ")", "*"}
for _, char := range unwantedChars {
s = strings.ReplaceAll(s, char, "")
}
return s
2023-06-04 17:22:25 +08:00
}
2023-08-11 10:00:36 +08:00
func updateCache() {
2024-04-02 23:21:40 +08:00
if time.Since(lastUpdateTime).Minutes() < 10 {
return
}
imgKey, subKey := getWbiKeys()
cache.Store("imgKey", imgKey)
cache.Store("subKey", subKey)
lastUpdateTime = time.Now()
2023-06-04 17:22:25 +08:00
}
2023-08-11 10:00:36 +08:00
func getWbiKeysCached() (string, string) {
2024-04-02 23:21:40 +08:00
updateCache()
imgKeyI, _ := cache.Load("imgKey")
subKeyI, _ := cache.Load("subKey")
return imgKeyI.(string), subKeyI.(string)
2023-06-04 17:22:25 +08:00
}
2023-08-11 10:00:36 +08:00
func getWbiKeys() (string, string) {
2024-04-02 23:21:40 +08:00
client := & http.Client{}
req, err := http.NewRequest("GET", "https://api.bilibili.com/x/web-interface/nav", nil)
if err != nil {
fmt.Printf("Error creating request: %s", err)
return "", ""
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Referer", "https://www.bilibili.com/")
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Error sending request: %s", err)
return "", ""
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response: %s", err)
return "", ""
}
json := string(body)
imgURL := gjson.Get(json, "data.wbi_img.img_url").String()
subURL := gjson.Get(json, "data.wbi_img.sub_url").String()
imgKey := strings.Split(strings.Split(imgURL, "/")[len(strings.Split(imgURL, "/"))-1], ".")[0]
subKey := strings.Split(strings.Split(subURL, "/")[len(strings.Split(subURL, "/"))-1], ".")[0]
return imgKey, subKey
2023-06-04 17:22:25 +08:00
}
```
2023-06-28 17:57:02 +08:00
2023-06-28 18:18:33 +08:00
### CSharp
2023-06-28 17:57:02 +08:00
2023-06-28 18:18:33 +08:00
无需依赖外部库
2023-06-28 17:57:02 +08:00
2023-06-28 18:18:33 +08:00
```cs
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Nodes;
2023-06-28 17:57:02 +08:00
2023-06-28 18:18:33 +08:00
class Program
{
private static HttpClient _httpClient = new();
2023-06-28 17:57:02 +08:00
2023-06-28 18:18:33 +08:00
private static readonly int[] 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
2023-06-28 17:57:02 +08:00
};
2023-06-28 18:18:33 +08:00
//对 imgKey 和 subKey 进行字符顺序打乱编码
private static string GetMixinKey(string orig)
{
return MixinKeyEncTab.Aggregate("", (s, i) => s + orig[i])[..32];
}
private static Dictionary< string , string > EncWbi(Dictionary< string , string > parameters, string imgKey,
string subKey)
{
string mixinKey = GetMixinKey(imgKey + subKey);
string currTime = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
//添加 wts 字段
parameters["wts"] = currTime;
// 按照 key 重排参数
parameters = parameters.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value);
//过滤 value 中的 "!'()*" 字符
parameters = parameters.ToDictionary(
kvp => kvp.Key,
kvp => new string(kvp.Value.Where(chr => !"!'()*".Contains(chr)).ToArray())
);
// 序列化参数
string query = new FormUrlEncodedContent(parameters).ReadAsStringAsync().Result;
//计算 w_rid
using MD5 md5 = MD5.Create();
byte[] hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(query + mixinKey));
string wbiSign = BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
parameters["w_rid"] = wbiSign;
return parameters;
}
// 获取最新的 img_key 和 sub_key
2024-04-02 23:21:40 +08:00
private static async Task< (string, string)> GetWbiKeys()
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
httpClient.DefaultRequestHeaders.Referrer = new Uri("https://www.bilibili.com/");
HttpResponseMessage responseMessage = await httpClient.SendAsync(new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://api.bilibili.com/x/web-interface/nav"),
});
JsonNode response = JsonNode.Parse(await responseMessage.Content.ReadAsStringAsync())!;
string imgUrl = (string)response["data"]!["wbi_img"]!["img_url"]!;
imgUrl = imgUrl.Split("/")[^1].Split(".")[0];
string subUrl = (string)response["data"]!["wbi_img"]!["sub_url"]!;
subUrl = subUrl.Split("/")[^1].Split(".")[0];
return (imgUrl, subUrl);
}
2023-06-28 18:18:33 +08:00
2023-06-28 17:57:02 +08:00
2023-06-28 18:18:33 +08:00
public static async Task Main()
{
var (imgKey, subKey) = await GetWbiKeys();
Dictionary< string , string > signedParams = EncWbi(
parameters: new Dictionary< string , string >
{
{ "foo", "114" },
{ "bar", "514" },
{ "baz", "1919810" }
},
imgKey: imgKey,
subKey: subKey
);
string query = await new FormUrlEncodedContent(signedParams).ReadAsStringAsync();
Console.WriteLine(query);
2023-06-28 17:57:02 +08:00
}
}
2023-06-28 18:18:33 +08:00
```
输出内容为进行 Wbi 签名的后参数的 url query 形式
```
bar=514& baz=1919810& foo=114& wts=1687541921& w_rid=26e82b1b9b3a11dbb1807a9228a40d3b
2023-06-30 12:27:43 +08:00
```
### Java
```java
2024-07-29 10:03:09 +08:00
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
2023-06-30 12:27:43 +08:00
import java.util.*;
2024-07-29 10:03:09 +08:00
import java.util.stream.Collectors;
2023-06-30 12:27:43 +08:00
public class WbiTest {
private static final int[] mixinKeyEncTab = new int[]{
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
};
2024-07-29 10:03:09 +08:00
private static final char[] hexDigits = "0123456789abcdef".toCharArray();
public static String md5(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(input.getBytes(StandardCharsets.UTF_8));
char[] result = new char[messageDigest.length * 2];
for (int i = 0; i < messageDigest.length ; i + + ) {
result[i * 2] = hexDigits[(messageDigest[i] >> 4) & 0xF];
result[i * 2 + 1] = hexDigits[messageDigest[i] & 0xF];
}
return new String(result);
} catch (NoSuchAlgorithmException e) {
return null;
}
}
2023-06-30 12:27:43 +08:00
public static String getMixinKey(String imgKey, String subKey) {
String s = imgKey + subKey;
StringBuilder key = new StringBuilder();
2024-07-29 10:03:09 +08:00
for (int i = 0; i < 32 ; i + + )
2023-06-30 12:27:43 +08:00
key.append(s.charAt(mixinKeyEncTab[i]));
return key.toString();
}
2024-07-29 10:03:09 +08:00
public static String encodeURIComponent(Object o) {
return URLEncoder.encode(o.toString(), StandardCharsets.UTF_8).replace("+", "%20");
}
public static void main(String[] args) {
String imgKey = "653657f524a547ac981ded72ea172057";
String subKey = "6e4909c702f846728e64f6007736a338";
String mixinKey = getMixinKey(imgKey, subKey);
System.out.println(mixinKey); // 72136226c6a73669787ee4fd02a74c27
// 用TreeMap自动排序
TreeMap< String , Object > map = new TreeMap< >();
map.put("foo", "one one four");
map.put("bar", "五一四");
map.put("baz", 1919810);
map.put("wts", System.currentTimeMillis() / 1000);
String param = map.entrySet().stream()
.map(it -> String.format("%s=%s", it.getKey(), encodeURIComponent(it.getValue())))
.collect(Collectors.joining("&"));
String s = param + mixinKey;
String wbiSign = md5(s);
System.out.println(wbiSign);
String finalParam = param + "& w_rid=" + wbiSign;
System.out.println(finalParam);
}
}
```
### Kotlin
2024-09-19 00:15:40 +08:00
说明: 为了便于使用和缓存, 重新编写为实体类形式, 并拆分了多个文件. 使用官方的JSON序列化. (可以根据需要换成其他的)
2024-07-29 10:03:09 +08:00
2024-09-19 00:15:40 +08:00
WbiParams.kt
2024-07-29 10:03:09 +08:00
2024-09-19 00:15:40 +08:00
```kotlin
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
private fun JsonElement?.get(): String {
check(this != null) { "No contents found" }
return this.jsonPrimitive.content.split('/').last().removeSuffix(".png")
2024-07-29 10:03:09 +08:00
}
private val mixinKeyEncTab = intArrayOf(
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
)
2024-09-19 00:15:40 +08:00
@Serializable
data class WbiParams(
val imgKey: String,
val subKey: String,
) {
// 此处整合了切分参数(直接传入{img_url:string, sub_url:string}即可), 不需要可以删掉
constructor(wbiImg: JsonObject) : this(wbiImg["img_url"].get(), wbiImg["sub_url"].get())
private val mixinKey: String
get() = (imgKey + subKey).let { s ->
buildString {
repeat(32) {
append(s[mixinKeyEncTab[it]])
}
}
}
// 创建对象(GET获取或者读缓存, 比如Redis)之后, 直接调用此函数处理
fun enc(params: Map< String , Any ? > ): String {
val sorted = params.filterValues { it != null }.toSortedMap()
return buildString {
append(sorted.toQueryString())
val wts = System.currentTimeMillis() / 1000
sorted["wts"] = wts
append("& wts=")
append(wts)
append("& w_rid=")
append((sorted.toQueryString() + mixinKey).toMD5())
2024-07-29 10:03:09 +08:00
}
}
}
2024-09-19 00:15:40 +08:00
```
2024-07-29 10:03:09 +08:00
2024-09-19 00:15:40 +08:00
Extensions.kt
2024-07-29 10:03:09 +08:00
2024-09-19 00:15:40 +08:00
```kotlin
import java.security.MessageDigest
private val hexDigits = "0123456789abcdef".toCharArray()
fun ByteArray.toHexString() = buildString(this.size shl 1) {
this@toHexString.forEach { byte ->
append(hexDigits[byte.toInt() ushr 4 and 15])
append(hexDigits[byte.toInt() and 15])
2024-07-29 10:03:09 +08:00
}
}
2024-09-19 00:15:40 +08:00
fun String.toMD5(): String {
val md = MessageDigest.getInstance("MD5")
val digest = md.digest(this.toByteArray())
return digest.toHexString()
}
2024-07-29 10:03:09 +08:00
2024-09-19 00:15:40 +08:00
fun Map< String , Any ? > .toQueryString() = this.filterValues { it != null }.entries.joinToString("& ") { (k, v) ->
"${k.encodeURIComponent()}=${v!!.encodeURIComponent()}"
2023-06-30 12:27:43 +08:00
}
2023-08-13 20:31:31 +08:00
```
2023-11-19 17:24:36 +08:00
2024-09-19 00:15:40 +08:00
获取和使用案例略
2023-11-19 17:24:36 +08:00
### PHP
来自[SocialSisterYi/bilibili-API-collect#813](https://github.com/SocialSisterYi/bilibili-API-collect/issues/813)
```php
< ?php
/**
* B站 Wbi 测试
* @author Prk
*/
class Bilibili {
private $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
];
function __construct() {
}
public function reQuery(array $query) {
$wbi_keys = $this->getWbiKeys();
return $this->encWbi($query, $wbi_keys['img_key'], $wbi_keys['sub_key']);
}
private function getMixinKey($orig) {
$t = '';
foreach ($this->mixinKeyEncTab as $n) $t .= $orig[$n];
return substr($t, 0, 32);
}
private function encWbi($params, $img_key, $sub_key) {
$mixin_key = $this->getMixinKey($img_key . $sub_key);
$curr_time = time();
$chr_filter = "/[!'()*]/";
$query = [];
$params['wts'] = $curr_time;
ksort($params);
foreach ($params as $key => $value) {
$value = preg_replace($chr_filter, '', $value);
$query[] = urlencode($key) . '=' . urlencode($value);
}
$query = implode('& ', $query);
$wbi_sign = md5($query . $mixin_key);
return $query . '& w_rid=' . $wbi_sign;
}
private function getWbiKeys() {
$resp = @json_decode (
$this->curl_get(
'https://api.bilibili.com/x/web-interface/nav',
null,
'https://www.bilibili.com/'
), true
);
if (!$resp) throw new Exception('请求失败');
$img_url = $resp['data']['wbi_img']['img_url'];
$sub_url = $resp['data']['wbi_img']['sub_url'];
return [
'img_key' => substr(basename($img_url), 0, strpos(basename($img_url), '.')),
'sub_key' => substr(basename($sub_url), 0, strpos(basename($sub_url), '.'))
];
}
private function curl_get($url, $cookies = null, $referer = 'https://www.bilibili.com/', $ua = null, $proxy = null, $header = []) {
$ch = curl_init();
$header[] = "Accept: */* ";
$header[] = "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7";
$header[] = "Connection: close";
2024-04-02 23:21:40 +08:00
$header[]="Referer:https://www.bilibili.com/";
2023-11-19 17:24:36 +08:00
$header[] = "Cache-Control: max-age=0";
curl_setopt_array($ch, [
CURLOPT_HTTPGET => 1,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HTTPHEADER => $header,
CURLOPT_ENCODING => '',
CURLOPT_URL => $url,
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 Edg/102.0.1245.39',
CURLOPT_TIMEOUT => 15
]);
if ($cookies) curl_setopt(
$ch,
CURLOPT_COOKIE,
$cookies
);
if ($referer) curl_setopt_array($ch, [
CURLOPT_AUTOREFERER => $referer,
CURLOPT_REFERER => $referer
]);
$content = curl_exec($ch);
curl_close($ch);
return $content;
}
}
$c = new Bilibili();
echo $c->reQuery(['foo' => '114', 'bar' => '514', 'baz' => 1919810]);
// bar=514& baz=1919810& foo=114& wts=1700384803& w_rid=4614cb98d60a43e50c3a3033fe3d116b
2023-12-03 23:01:43 +08:00
```
### Rust
需要 serde、serde_json、reqwest、tokio 以及 md5
```rust
use reqwest::header::USER_AGENT;
use serde::Deserialize;
use std::time::{SystemTime, UNIX_EPOCH};
const MIXIN_KEY_ENC_TAB: [usize; 64] = [
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,
];
#[derive(Deserialize)]
struct WbiImg {
img_url: String,
sub_url: String,
}
#[derive(Deserialize)]
struct Data {
wbi_img: WbiImg,
}
2023-11-19 17:24:36 +08:00
2023-12-03 23:01:43 +08:00
#[derive(Deserialize)]
struct ResWbi {
data: Data,
}
// 对 imgKey 和 subKey 进行字符顺序打乱编码
fn get_mixin_key(orig: & [u8]) -> String {
MIXIN_KEY_ENC_TAB
.iter()
2024-07-20 17:53:25 +08:00
.take(32)
2023-12-03 23:01:43 +08:00
.map(|& i| orig[i] as char)
.collect::< String > ()
}
fn get_url_encoded(s: & str) -> String {
s.chars()
.filter_map(|c| match c.is_ascii_alphanumeric() || "-_.~".contains(c) {
true => Some(c.to_string()),
false => {
// 过滤 value 中的 "!'()*" 字符
if "!'()*".contains(c) {
return None;
}
let encoded = c
.encode_utf8(& mut [0; 4])
.bytes()
.fold("".to_string(), |acc, b| acc + & format!("%{:02X}", b));
Some(encoded)
}
})
.collect::< String > ()
}
// 为请求参数进行 wbi 签名
2024-07-20 17:53:25 +08:00
fn encode_wbi(params: Vec< (& str, String)>, (img_key, sub_key): (String, String)) -> String {
2023-12-03 23:01:43 +08:00
let cur_time = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(t) => t.as_secs(),
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
};
2024-07-20 17:53:25 +08:00
_encode_wbi(params, (img_key, sub_key), cur_time)
}
fn _encode_wbi(
mut params: Vec< (& str, String)>,
(img_key, sub_key): (String, String),
timestamp: u64,
) -> String {
let mixin_key = get_mixin_key((img_key + &sub_key).as_bytes());
2023-12-03 23:01:43 +08:00
// 添加当前时间戳
2024-07-20 17:53:25 +08:00
params.push(("wts", timestamp.to_string()));
2023-12-03 23:01:43 +08:00
// 重新排序
params.sort_by(|a, b| a.0.cmp(b.0));
2024-07-20 17:53:25 +08:00
// 拼接参数
let query = params
.iter()
.map(|(k, v)| format!("{}={}", get_url_encoded(k), get_url_encoded(v)))
.collect::< Vec < _ > >()
.join("&");
// 计算签名
2023-12-03 23:01:43 +08:00
let web_sign = format!("{:?}", md5::compute(query.clone() + &mixin_key));
2024-07-20 17:53:25 +08:00
// 返回最终的 query
query + & format!("& w_rid={}", web_sign)
2023-12-03 23:01:43 +08:00
}
async fn get_wbi_keys() -> Result< (String, String), reqwest::Error> {
let client = reqwest::Client::new();
let ResWbi { data:Data{wbi_img} } = client
.get("https://api.bilibili.com/x/web-interface/nav")
.header(USER_AGENT,"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36")
2024-04-02 23:21:40 +08:00
.header("Referer","https://www.bilibili.com/")
2023-12-03 23:01:43 +08:00
// SESSDATA=xxxxx
.header("Cookie", "SESSDATA=xxxxx")
.send()
.await?
.json::< ResWbi > ()
.await?;
2024-07-20 17:53:25 +08:00
Ok((
take_filename(wbi_img.img_url).unwrap(),
take_filename(wbi_img.sub_url).unwrap(),
))
}
2023-12-03 23:01:43 +08:00
2024-07-20 17:53:25 +08:00
fn take_filename(url: String) -> Option< String > {
url.rsplit_once('/')
.and_then(|(_, s)| s.rsplit_once('.'))
.map(|(s, _)| s.to_string())
2023-12-03 23:01:43 +08:00
}
#[tokio::main]
async fn main() {
2024-07-20 17:53:25 +08:00
let keys = get_wbi_keys().await.unwrap();
let params = vec![
2023-12-03 23:01:43 +08:00
("foo", String::from("114")),
("bar", String::from("514")),
("baz", String::from("1919810")),
];
2024-07-20 17:53:25 +08:00
let query = encode_wbi(params, keys);
println!("{}", query);
}
2023-12-03 23:01:43 +08:00
2024-07-20 17:53:25 +08:00
// 取自文档描述的测试用例
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_filename() {
assert_eq!(
take_filename(
"https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png".to_string()
),
Some("7cd084941338484aae1ad9425b84077c".to_string())
);
}
2023-12-03 23:01:43 +08:00
2024-07-20 17:53:25 +08:00
#[test]
fn test_get_mixin_key() {
let concat_key =
"7cd084941338484aae1ad9425b84077c".to_string() + "4932caff0ff746eab6f01bf08b70ac45";
assert_eq!(
get_mixin_key(concat_key.as_bytes()),
"ea1db124af3c7062474693fa704f4ff8"
);
}
#[test]
fn test_encode_wbi() {
let params = vec![
("foo", String::from("114")),
("bar", String::from("514")),
("zab", String::from("1919810")),
];
assert_eq!(
_encode_wbi(
params,
(
"7cd084941338484aae1ad9425b84077c".to_string(),
"4932caff0ff746eab6f01bf08b70ac45".to_string()
),
1702204169
),
"bar=514& foo=114& wts=1702204169& zab=1919810& w_rid=8f6f2b5b3d485fe1886cec6a0be8c5d4"
.to_string()
)
}
2023-12-03 23:01:43 +08:00
}
2023-11-19 17:24:36 +08:00
```
2023-12-03 23:01:43 +08:00
2023-12-10 10:03:53 +08:00
### Swift
需要 [Alamofire ](https://github.com/Alamofire/Alamofire ) 和 [SwiftyJSON ](https://github.com/SwiftyJSON/SwiftyJSON ) 库
```swift
import Foundation
import CommonCrypto
import Alamofire
import SwiftyJSON
func biliWbiSign(param: String, completion: @escaping (String?) -> Void) {
func getMixinKey(orig: String) -> String {
return String(mixinKeyEncTab.map { orig[orig.index(orig.startIndex, offsetBy: $0)] }.prefix(32))
}
func encWbi(params: [String: Any], imgKey: String, subKey: String) -> [String: Any] {
var params = params
let mixinKey = getMixinKey(orig: imgKey + subKey)
let currTime = round(Date().timeIntervalSince1970)
params["wts"] = currTime
params = params.sorted { $0.key < $1.key }.reduce(into: [:]) { $0[$1.key] = $1.value }
params = params.mapValues { String(describing: $0).filter { !"!'()*".contains($0) } }
let query = params.map { "\($0.key)=\($0.value)" }.joined(separator: "& ")
let wbiSign = calculateMD5(string: query + mixinKey)
params["w_rid"] = wbiSign
return params
}
2024-04-02 23:21:40 +08:00
func getWbiKeys(completion: @escaping (Result< (imgKey: String, subKey: String), Error>) -> Void) {
let headers: HTTPHeaders = [
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Referer": "https://www.bilibili.com/"
]
AF.request("https://api.bilibili.com/x/web-interface/nav", headers: headers).responseJSON { response in
switch response.result {
case .success(let value):
let json = JSON(value)
let imgURL = json["data"]["wbi_img"]["img_url"].string ?? ""
let subURL = json["data"]["wbi_img"]["sub_url"].string ?? ""
let imgKey = imgURL.components(separatedBy: "/").last?.components(separatedBy: ".").first ?? ""
let subKey = subURL.components(separatedBy: "/").last?.components(separatedBy: ".").first ?? ""
completion(.success((imgKey, subKey)))
case .failure(let error):
completion(.failure(error))
}
}
}
2023-12-10 10:03:53 +08:00
func calculateMD5(string: String) -> String {
let data = Data(string.utf8)
var digest = [UInt8 ](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH ))
_ = data.withUnsafeBytes {
CC_MD5($0.baseAddress, CC_LONG(data.count), & digest)
}
return digest.map { String(format: "%02hhx", $0) }.joined()
}
let 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
]
getWbiKeys { result in
switch result {
case .success(let keys):
let spdParam = param.components(separatedBy: "& ")
var spdDicParam = [String: String]()
spdParam.forEach { pair in
let components = pair.components(separatedBy: "=")
if components.count == 2 {
spdDicParam[components[0]] = components[1]
}
}
let signedParams = encWbi(params: spdDicParam, imgKey: keys.imgKey, subKey: keys.subKey)
let query = signedParams.map { "\($0.key)=\($0.value)" }.joined(separator: "& ")
completion(query)
case .failure(let error):
print("Error getting keys: \(error)")
completion(nil)
}
}
}
```
2024-06-13 10:26:03 +08:00
### CPlusPlus
需要 c++ 23 标准库,[cpr](https://github.com/libcpr/cpr)、[cryptopp](https://github.com/weidai11/cryptopp)、[nlohmann/json](https://github.com/nlohmann/json) 等依赖
```c++
#include <array> // std::array
#include <locale> // std::locale
#include <print> // std::println
/// thrid party libraries
#include <cpr/cpr.h>
#include <cryptopp/md5.h>
#include <cryptopp/hex.h>
#include <nlohmann/json.hpp>
/*
* 注意,假定不会发生错误!
*/
class Wbi {
constexpr static std::array< uint8_t , 64 > MIXIN_KEY_ENC_TAB_ = {
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
};
/* 获取 md5 hex(lower) */
static std::string Get_md5_hex(const std::string & Input_str) {
CryptoPP::Weak1::MD5 hash;
std::string md5_hex;
CryptoPP::StringSource ss(Input_str, true,
new CryptoPP::HashFilter(hash,
new CryptoPP::HexEncoder(
new CryptoPP::StringSink(md5_hex)
)
)
);
std::ranges::for_each(md5_hex, [](char & x) { x = std::tolower(x); });
return md5_hex;
}
public:
/* 将 json 转换为 url 编码字符串 */
static std::string Json_to_url_encode_str(const nlohmann::json & Json) {
std::string encode_str;
for (const auto & [key, value]: Json.items()) {
encode_str.append(key).append("=").append(cpr::util::urlEncode(value.is_string() ? value.get< std::string > () : to_string(value))).append("&");
}
// remove the last '& '
encode_str.resize(encode_str.size() - 1, '\0');
return encode_str;
}
/* 获取 wbi key */
static std::pair< std::string , std::string > Get_wbi_key() {
const auto url = cpr::Url {"https://api.bilibili.com/x/web-interface/nav"};
const auto cookie = cpr::Cookies {
{"SESSDATA", "xxxxxxxxxxxx"},
};
const auto header = cpr::Header {
{"User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"},
{"Referer", "https://www.bilibili.com/"},
};
const auto response = cpr::Get(url, cookie, header);
nlohmann::json json = nlohmann::json::parse(response.text);
const std::string img_url = json["data"]["wbi_img"]["img_url"];
const std::string sub_url = json["data"]["wbi_img"]["sub_url"];
std::string img_key = img_url.substr(img_url.find("wbi/") + 4, img_url.find(".png") - img_url.find("wbi/") - 4);
std::string sub_key = sub_url.substr(sub_url.find("wbi/") + 4, sub_url.find(".png") - sub_url.find("wbi/") - 4);
return {img_key, sub_key};
}
/* 获取 mixin key */
static std::string Get_mixin_key(const std::string & Img_key, const std::string & Sub_key) {
std::string raw_wbi_key_str = Img_key + Sub_key;
std::string result;
std::ranges::for_each(MIXIN_KEY_ENC_TAB_, [&result, &raw_wbi_key_str ](const uint8_t x ) {
result.push_back(raw_wbi_key_str.at(x));
});
return result.substr(0, 32);
}
/* 计算签名(w_rid) */
static std::string Calc_sign(nlohmann::json & Params, const std::string & Mixin_key) {
Params["wts"] = std::chrono::duration_cast< std::chrono::seconds > (std::chrono::system_clock::now().time_since_epoch()).count();
const std::string encode_str = Json_to_url_encode_str(Params).append(Mixin_key);
return Get_md5_hex(encode_str);
}
};
int main() {
nlohmann::json Params;
// qn=32& fnver=0& fnval=4048& fourk=1& avid=1755630705& cid=1574294582
Params["qn"] = 32;
Params["fnver"] = 0;
Params["fnval"] = 4048;
Params["fourk"] = 1;
Params["avid"] = 1755630705;
Params["cid"] = 1574294582;
auto [img_key, sub_key] = Wbi::Get_wbi_key();
const auto mixin_key = Wbi::Get_mixin_key(img_key, sub_key);
const auto w_rid = Wbi::Calc_sign(Params, mixin_key);
std::println("{}", Wbi::Json_to_url_encode_str(Params) + "& w_rid=" + w_rid);
}
```
```text
avid=1755630705& cid=1574294582& fnval=4048& fnver=0& fourk=1& qn=32& wts=1717922933& w_rid=43571b838a1611fa121189083cfc1784
2024-07-29 10:03:09 +08:00
```