Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bili_ticket 算法 Java 实现 及 信息补充 及 错误修正 #1061

Merged
merged 11 commits into from
Jul 25, 2024
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,13 @@ B站 API 采用 C/S 结构,大多数接口为 REST API 和 gRPC,少部分接
- [x] [APP API 签名](docs/misc/sign/APP.md)(`appkey`与`sign`)
- [x] [已知的 APPKey](docs/misc/sign/APPKey.md)
- [x] [Wbi 签名](docs/misc/sign/wbi.md)(`wts`与`w_rid`)
- [ ] [bili_ticket](docs/misc/sign/bili_ticket.md)

- [x] [公共错误码](docs/misc/errcode.md)
- [x] [图片格式化](docs/misc/picture.md)
- [x] [bvid 说明](docs/misc/bvid_desc.md)
- [ ] [设备唯一标识BUVID](docs/misc/device_identity.md)
- [ ] [获取 buvid3 / buvid4](docs/misc/buvid3_4.md)
- [ ] [gRPC API 接口定义](grpc_api)
- [ ] [登录](docs/login)
- [x] [登录操作 (人机认证)](docs/login/login_action)
Expand Down Expand Up @@ -207,6 +209,7 @@ B站 API 采用 C/S 结构,大多数接口为 REST API 和 gRPC,少部分接
- [ ] 动态列表
- [x] [特定话题动态列表](docs/dynamic/tag_dynamics.md)
- [ ] [动态内容](docs/dynamic/get_dynamic_detail.md)
- [ ] [导航栏动态](docs/dynamic/nav.md)
- [ ] [相簿](docs/album)
- [x] [基本信息](docs/album/info.md)
- [x] [相簿列表](docs/album/list.md)
Expand Down Expand Up @@ -357,6 +360,7 @@ OR Aifadian:[https://afdian.net/@ShakaiAneE](https://afdian.net/@ShakaiAneE)
- [SocialSisterYi/bcut-asr](https://github.com/SocialSisterYi/bcut-asr): 使用必剪API的语音字幕识别
- [CzJam/Bili_Realtime_Data](https://github.com/CzJam/Bili_Realtime_Data): Bilibili粉丝与视频实时数据统计
- [kingwingfly/fav](https://github.com/kingwingfly/fav): 自动同步bili收藏夹、合集视频到本地的CLI工具(Rust实现,并提供一个文档测试完善的Rust风格的用于构建有状态爬虫的核心库)
- [linyuye/Bilibili_crawler](https://github.com/linyuye/Bilibili_crawler): 基于bilibili懒加载api爬取b站动态,视频等评论区

### 其他

Expand Down
92 changes: 91 additions & 1 deletion docs/clientinfo/ip.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 通过ip确定地理位置

## 通过ip确定位置
## 根据请求IP确定属地

> https://api.bilibili.com/x/web-interface/zone

Expand Down Expand Up @@ -60,3 +60,93 @@ curl 'https://api.bilibili.com/x/web-interface/zone'
```

</details>

## 查询任意 IP 地址的归属地

> https://api.live.bilibili.com/ip_service/v1/ip_service/get_ip_addr
> https://api.live.bilibili.com/client/v1/Ip/getInfoNew

注: 两接口等效

*请求方式:GET*

**URL参数:**


| 参数名 | 类型 | 内容 | 必要性 | 备注 |
| ------ | ---- | -------- | ------ | ---- |
| ip | str | IP地址 | 不必要 | IPv4或IPv6地址不限, 留空与[根据请求IP确定地理位置](#根据请求ip确定地理位置)基本相同 |

**JSON回复:**

根对象:

| 字段 | 类型 | 内容 | 备注 |
| ------- | ---- | -------- | ------- |
| code | num | 返回值 | 0: 成功 |
| message | str | 错误信息 | 默认为空 |
| msg | str | 错误信息 | 同message |
| data | obj | 信息本体 | 出错时为空数组 |

`data`对象:

与[根据请求IP确定地理位置](#根据请求ip确定地理位置)回复的`data`对象基本相同, 但无 `country_code` 字段

**示例:**

查询请求IP地址的归属地:

```shell
curl -G 'https://api.live.bilibili.com/client/v1/Ip/getInfoNew'
```

<details>
<summary>查看响应示例:</summary>


```json
{
"code": 0,
"msg": "",
"message": "",
"data": {
"addr": "104.28.156.113",
"country": "新加坡",
"province": "新加坡",
"city": "",
"isp": "cloudflare.com",
"latitude": "1.352083",
"longitude": "103.819836"
}
```

</details>

查询IP地址`8.8.8.8`的归属地:

```shell
curl -G 'https://api.live.bilibili.com/ip_service/v1/ip_service/get_ip_addr' \
--data-urlencode 'ip=8.8.8.8'
```

<details>
<summary>查看响应示例:</summary>

```json
{
"code": 0,
"msg": "",
"message": "",
"data": {
"addr": "8.8.8.8",
"country": "GOOGLE.COM",
"province": "GOOGLE.COM",
"city": "",
"isp": "level3.com",
"latitude": "",
"longitude": ""
}
}
```

</details>
2 changes: 1 addition & 1 deletion docs/fav/info.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

| 字段 | 类型 | 内容 | 备注 |
| ------- | ----------------------------- | -------- | --------------------------------------------------- |
| code | num | 返回值 | 0:成功<br />-400:请求错误<br />-403:访问权限不足 |
| code | num | 返回值 | 0:成功<br />-400:请求错误<br />-403:访问权限不足<br />11010: 内容不存在 |
| message | str | 错误信息 | 默认为0 |
| data | 有效时:obj<br />无效或:null | 信息本体 | |

Expand Down
48 changes: 48 additions & 0 deletions docs/misc/buvid3_4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 获取 buvid3 / buvid4

## 游客获取 buvid3 / buvid4

> https://api.bilibili.com/x/frontend/finger/spi

*请求方式: GET*

**JSON回复:**

根对象:

| 字段 | 类型 | 内容 | 备注 |
| ------- | ---- | -------- | -------- |
| code | num | 返回值 | 0:成功 |
| message | str | 信息 | ok: 成功 |
| data | obj | 数据本体 | |

`data`对象:

| 字段 | 类型 | 内容 | 备注 |
| ---- | ---- | ------ | ---- |
| b_3 | str | buvid3 | 需手动存放至 cookie 中 |
| b_4 | str | buvid4 | 同上 |

**示例:**

注: 建议自行生成, 不要复制本处示例的 buvid3 / buvid4.

```shell
curl -G 'https://api.bilibili.com/x/frontend/finger/spi'
```

<details>
<summary>查看响应示例:</summary>

```json
{
"code": 0,
"data": {
"b_3": "D9656DA8-9BEF-F464-5B72-C4849AFD336379044infoc",
"b_4": "F6E0FD4B-520C-1902-4F7B-E461D8D1F5AB79044-024072309-666onEZSnlHVPjoRp4kDYg=="
},
"message": "ok"
}
```

</details>
124 changes: 120 additions & 4 deletions docs/misc/sign/bili_ticket.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
`bili_ticket` 目前没发现多少风控价值,但是暂且在这里提供一份示例。
# BiliTicket

由 [@aynuarance](https://github.com/aynuarance) 于 [#903](https://github.com/SocialSisterYi/bilibili-API-collect/issues/903) 提供的思路,根据时间戳使用 `hmac_sha256` 算法计算 `hexsign`。
## 简述

`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`
Expand All @@ -14,7 +17,18 @@
}
```

# Python 示例
## 算法

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` 字段的值即为所求

## Demo

### Python

需要 `requests` 依赖

```python
import hmac
Expand Down Expand Up @@ -59,4 +73,106 @@ if __name__ == '__main__':
'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();
}
}

}
```
2 changes: 0 additions & 2 deletions docs/misc/sign/wbi.md
Original file line number Diff line number Diff line change
Expand Up @@ -700,8 +700,6 @@ public class WbiTest {
map.put("bar", "五一四");
map.put("baz", 1919810);
map.put("wts", System.currentTimeMillis() / 1000);
map.put("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");
map.put("Referer", "https://www.bilibili.com/");
StringJoiner param = new StringJoiner("&");
//排序 + 拼接字符串
map.entrySet().stream()
Expand Down
Loading