* fix(video/collection.md): unclosed xml tags * feat(video/report.md): uuid not really random * feat: b23.tv short link * feat: login/moral/exp log * feat: v_voucher * feat: upload video cover & post * feat(creativecenter/upload.md): upload octet-stream * feat(creativecenter/upload.md): update some notes * feat(clientinfo/ip.md): another from live * feat: live web heartbeat * feat: update popular series & precious * fix(creativecenter/upload.md): mistakes in example * feat: merge duplicate fav info * feat(README.md): missing link to webmask * feat(search/hot.md): word_type * feat(login/login_action): recovery old api * feat(video/collection.md): series operation * feat: video season operation * feat: add & mod some links * feat(search/suggest.md): up to date * feat: web home header image * feat(misc/sign/bili_ticket.md): demo for nodejs * feat(creativecenter/upload.md): x-upos-auth validity period * feat: add referer & buvid3 to request header * feat: customer service message heartbeat & upload * feat(customerservice/msg.md): upload code 1200201 * feat(video/report.md): click/web/h5 * feat(video/report.md): view count desc * feat: laser2 * feat: wbi_key in bili_ticket * fix: typo & unclosed XML tags * feat(misc/sign/v_voucher): cookie x-bili-gaia-vtoken * feat(search/hot.md): square * feat(video/status_number.md): fold archive_stat/stat * feat(fav/info.md): resource/infos invalid type 21 * feat: /x/activity/subject/info * feat: lottery * feat(docs/misc/b23.tv): remove some unnecessary fields * feat(creativecenter/upload.md): types/predict * fix(video/collection.md): invalid end tag * feat: app version upgrade * feat(creativecenter/upload.md): tag/recommend #528 * feat(user/status_number.md): navnum * feat: /x/activity/page/list * feat(comment/list.md): desc about pagination_str * feat(comment/list.md): update example * feat(dynamic/all.md): #1082 * fix(comment/list.md): -352 not -412 * feat: #700 * feat(video/video_stream.md): #606 & cv949156 * feat(message/private_msg.md): single_unread freq * feat: getUserWallet * fix: broken form * feat(Layout.vue): copyright to 2024 * feat: /x/topic/pub/rcmd/search * feat: #425 * feat(misc/time_stamp.md): rtc/getTimestamp * fix(misc/time_stamp.md): missing end tag * feat: #745 * feat(dynamic/all.md): update feed/all * feat(danmaku/action.md): #220 * feat(live/info.md): gethistory from cv8186413 * fix(danmaku/action.md): missing end tag
308 lines
9.4 KiB
Markdown
308 lines
9.4 KiB
Markdown
# BiliTicket
|
||
|
||
## 简述
|
||
|
||
`bili_ticket` 位于请求头 Cookie 中, 非必需, 但存在可降低风控概率
|
||
|
||
由 [@aynuarance](https://github.com/aynuarance) 于 [#903](https://github.com/SocialSisterYi/bilibili-API-collect/issues/903) 提供的思路,根据时间戳使用 `hmac_sha256` 算法计算 `hexsign`。
|
||
|
||
是 [JWT 令牌](https://jwt.io/),有效时长为 259260 秒,即 3 天。
|
||
例如 `eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDI3NDI3NDYsImlhdCI6MTcwMjQ4MzQ4NiwicGx0IjotMX0.xQgtTAc41NA1gzvd9yKUPgucUy_DKcQj6OG1vj8V7ZA`
|
||
|
||
```json
|
||
{
|
||
"alg": "HS256",
|
||
"kid": "s03",
|
||
"typ": "JWT"
|
||
}
|
||
```
|
||
|
||
## 算法
|
||
|
||
1. 获取 UNIX 秒级时间戳存入变量如 `timestamp`
|
||
2. 计算变量 `hexsign` 值,使用 `hmac_sha256` 算法,密钥为 `XgwSnGZ1p`,消息为字符串 `"ts"` 与变量 `timestamp` 值拼接
|
||
3. 构造请求参数,`key_id` 为 `ec02`,`hexsign` 为变量 `hexsign` 值,`context[ts]` 为变量 `timestamp` 值,`csrf` 为 cookie 中的 `bili_jct` 值也可为空
|
||
4. 发送 `POST` 请求,获取 `data` 字段中的 `ticket` 字段的值即为所求
|
||
|
||
## 接口
|
||
|
||
> https://api.bilibili.com/bapis/bilibili.api.ticket.v1.Ticket/GenWebTicket
|
||
|
||
*请求方式: POST*
|
||
|
||
**URL参数:**
|
||
|
||
| 参数名 | 类型 | 内容 | 必要性 | 备注 |
|
||
| ----- | ---- | ---- | ------ | ---- |
|
||
| key_id | str | ec02 | 必要 | |
|
||
| hexsign | str | 由 `hmac_sha256` 算法计算的 `hexsign` 值 | 必要 | |
|
||
| context[ts] | num | UNIX 秒级时间戳 | 必要 | |
|
||
| csrf | str | cookie 中的 `bili_jct` 值 | 非必要 | |
|
||
|
||
**JSON回复:**
|
||
|
||
根对象:
|
||
|
||
| 字段 | 类型 | 内容 | 备注 |
|
||
| ---- | ---- | ---- | ---- |
|
||
| code | num | 返回值 | 0: 成功<br />400: 参数错误 |
|
||
| message | str | 返回消息 | OK: 成功 |
|
||
| data | obj | 数据本体 | |
|
||
| ttl | num | 1 | |
|
||
|
||
`data` 对象:
|
||
|
||
| 字段 | 类型 | 内容 | 备注 |
|
||
| ---- | ---- | ---- | ---- |
|
||
| ticket | str | bili_ticket | |
|
||
| created_at | num | 创建时间 | UNIX 秒级时间戳 |
|
||
| ttl | num | 有效时长 | 259200 秒 (3 天) |
|
||
| context | obj | 空 | |
|
||
| nav | obj | wbi_img 相关 | 参见 [WBI 签名](./wbi.md) |
|
||
|
||
`nav` 对象:
|
||
|
||
| 字段 | 类型 | 内容 | 备注 |
|
||
| ---- | ---- | ---- | ---- |
|
||
| img | str | img_key 值 | 参见 [WBI 签名](./wbi.md) |
|
||
| sub | str | sub_key 值 | 参见 [WBI 签名](./wbi.md) |
|
||
|
||
**示例:**
|
||
|
||
<details>
|
||
<summary>查看响应示例:</summary>
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "OK",
|
||
"data": {
|
||
"ticket": "eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjM2OTMwODAsImlhdCI6MTcyMzQzMzgyMCwicGx0IjotMX0.efOwv7i4m0ykABrXEDHGAechU2AByMcP_-3EYpQrNKs",
|
||
"created_at": 1723433820,
|
||
"ttl": 259200,
|
||
"context": {},
|
||
"nav": {
|
||
"img": "https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png",
|
||
"sub": "https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png"
|
||
}
|
||
},
|
||
"ttl": 1
|
||
}
|
||
```
|
||
|
||
</details>
|
||
|
||
## Demo
|
||
|
||
此处提供 [Python](#python), [Java](#java), [JavaScript (Node.js)](#javascript-nodejs) 的示例代码
|
||
|
||
### Python
|
||
|
||
需要 `requests` 依赖
|
||
|
||
```python
|
||
import hmac
|
||
import hashlib
|
||
import requests
|
||
import time
|
||
|
||
def hmac_sha256(key, message):
|
||
"""
|
||
使用HMAC-SHA256算法对给定的消息进行加密
|
||
:param key: 密钥
|
||
:param message: 要加密的消息
|
||
:return: 加密后的哈希值
|
||
"""
|
||
# 将密钥和消息转换为字节串
|
||
key = key.encode('utf-8')
|
||
message = message.encode('utf-8')
|
||
|
||
# 创建HMAC对象,使用SHA256哈希算法
|
||
hmac_obj = hmac.new(key, message, hashlib.sha256)
|
||
|
||
# 计算哈希值
|
||
hash_value = hmac_obj.digest()
|
||
|
||
# 将哈希值转换为十六进制字符串
|
||
hash_hex = hash_value.hex()
|
||
|
||
return hash_hex
|
||
|
||
|
||
if __name__ == '__main__':
|
||
o = hmac_sha256("XgwSnGZ1p",f"ts{int(time.time())}")
|
||
url = "https://api.bilibili.com/bapis/bilibili.api.ticket.v1.Ticket/GenWebTicket"
|
||
params = {
|
||
"key_id":"ec02",
|
||
"hexsign":o,
|
||
"context[ts]":f"{int(time.time())}",
|
||
"csrf": ''
|
||
}
|
||
|
||
headers = {
|
||
'user-agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"
|
||
}
|
||
resp = requests.post(url, params=params,headers=headers).json()
|
||
print(resp)
|
||
```
|
||
|
||
### Java
|
||
|
||
无需第三方依赖
|
||
|
||
```java
|
||
import java.io.ByteArrayOutputStream;
|
||
import java.io.InputStream;
|
||
import java.net.HttpURLConnection;
|
||
import java.net.URI;
|
||
import java.nio.charset.StandardCharsets;
|
||
|
||
import javax.crypto.Mac;
|
||
import javax.crypto.spec.SecretKeySpec;
|
||
|
||
public class BiliTicketDemo {
|
||
|
||
/**
|
||
* Convert a byte array to a hex string.
|
||
*
|
||
* @param bytes The byte array to convert.
|
||
* @return The hex string representation of the given byte array.
|
||
*/
|
||
public static String bytesToHex(byte[] bytes) {
|
||
StringBuilder sb = new StringBuilder();
|
||
for (byte b : bytes) {
|
||
String hex = Integer.toHexString(0xff & b);
|
||
if (hex.length() == 1) {
|
||
sb.append('0');
|
||
}
|
||
sb.append(hex);
|
||
}
|
||
return sb.toString();
|
||
}
|
||
|
||
/**
|
||
* Generate a HMAC-SHA256 hash of the given message string using the given key
|
||
* string.
|
||
*
|
||
* @param key The key string to use for the HMAC-SHA256 hash.
|
||
* @param message The message string to hash.
|
||
* @throws Exception If an error occurs during the HMAC-SHA256 hash generation.
|
||
* @return The HMAC-SHA256 hash of the given message string using the given key
|
||
* string.
|
||
*/
|
||
public static String hmacSha256(String key, String message) throws Exception {
|
||
Mac mac = Mac.getInstance("HmacSHA256");
|
||
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||
mac.init(secretKeySpec);
|
||
byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
|
||
return bytesToHex(hash);
|
||
}
|
||
|
||
/**
|
||
* Get a Bilibili web ticket for the given CSRF token.
|
||
*
|
||
* @param csrf The CSRF token to use for the web ticket, can be {@code null} or
|
||
* empty.
|
||
* @return The Bilibili web ticket raw response for the given CSRF token.
|
||
* @throws Exception If an error occurs during the web ticket generation.
|
||
* @see https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/bili_ticket.md
|
||
*/
|
||
public static String getBiliTicket(String csrf) throws Exception {
|
||
// params
|
||
long ts = System.currentTimeMillis() / 1000;
|
||
String hexSign = hmacSha256("XgwSnGZ1p", "ts" + ts);
|
||
StringBuilder url = new StringBuilder(
|
||
"https://api.bilibili.com/bapis/bilibili.api.ticket.v1.Ticket/GenWebTicket");
|
||
url.append('?');
|
||
url.append("key_id=ec02").append('&');
|
||
url.append("hexsign=").append(hexSign).append('&');
|
||
url.append("context[ts]=").append(ts).append('&');
|
||
url.append("csrf=").append(csrf == null ? "" : csrf);
|
||
// request
|
||
HttpURLConnection conn = (HttpURLConnection) new URI(url.toString()).toURL().openConnection();
|
||
conn.setRequestMethod("POST");
|
||
conn.addRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0");
|
||
InputStream in = conn.getInputStream();
|
||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||
int b;
|
||
while ((b = in.read()) != -1) {
|
||
out.write(b);
|
||
}
|
||
return new String(out.toByteArray(), StandardCharsets.UTF_8);
|
||
}
|
||
|
||
/**
|
||
* Main method to test the BiliTicketDemo class.
|
||
*
|
||
* @param args The command line arguments (not used).
|
||
*/
|
||
public static void main(String[] args) {
|
||
try {
|
||
System.out.println(getBiliTicket("")); // use empty CSRF here
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
|
||
}
|
||
```
|
||
|
||
### JavaScript (Node.js)
|
||
|
||
```javascript
|
||
const crypto = require('crypto');
|
||
|
||
/**
|
||
* Generate HMAC-SHA256 signature
|
||
* @param {string} key The key string to use for the HMAC-SHA256 hash
|
||
* @param {string} message The message string to hash
|
||
* @returns {string} The HMAC-SHA256 signature as a hex string
|
||
*/
|
||
function hmacSha256(key, message) {
|
||
const hmac = crypto.createHmac('sha256', key);
|
||
hmac.update(message);
|
||
return hmac.digest('hex');
|
||
}
|
||
|
||
/**
|
||
* Get Bilibili web ticket
|
||
* @param {string} csrf CSRF token, can be empty or null
|
||
* @returns {Promise<any>} Promise of the ticket response in JSON format
|
||
*/
|
||
async function getBiliTicket(csrf) {
|
||
const ts = Math.floor(Date.now() / 1000);
|
||
const hexSign = hmacSha256('XgwSnGZ1p', `ts${ts}`);
|
||
const url = 'https://api.bilibili.com/bapis/bilibili.api.ticket.v1.Ticket/GenWebTicket';
|
||
const params = new URLSearchParams({
|
||
key_id: 'ec02',
|
||
hexsign: hexSign,
|
||
'context[ts]': ts,
|
||
csrf: csrf || ''
|
||
});
|
||
try {
|
||
const response = await fetch(`${url}?${params.toString()}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0'
|
||
}
|
||
});
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
const data = await response.json();
|
||
return data;
|
||
} catch (e) {
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
(async () => {
|
||
try {
|
||
const ticketResponse = await getBiliTicket(''); // use empty CSRF here
|
||
console.log(ticketResponse);
|
||
} catch (e) {
|
||
console.error('Failed to get BiliTicket:', error);
|
||
}
|
||
})();
|
||
```
|