概述
bilibili小程序开放平台提供消息订阅能力,开发者可以订阅平台推送的各类消息通知。当相关事件发生时,平台会通过HTTP POST回调的方式将消息推送到开发者配置的回调地址。
1 回调方式
1.1 回调请求格式
当订阅的消息产生时,平台会向开发者配置的 notify_url 发送 POST 请求。
请求URL格式:
{notify_url}?sign={sign}&ts={ts}&msg_id={msg_id}&msg_type={msg_type}
URL参数说明:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| sign | String | 是 | 签名,用于验证消息的真实性和完整性 |
| ts | Long | 是 | 时间戳(单位:毫秒),请求发起时的时间 |
| msg_id | Long | 是 | 消息唯一标识,用于消息去重和追踪 |
| msg_type | String | 是 | 消息类型,标识消息的业务类型 |
请求Body:
请求Body为JSON格式的消息内容,具体字段根据不同的消息类型而定。
示例:
POST https://your-domain.com/callback?sign=WbGNoWSnhogpKzilnQfPciPYdJgiTc2w6T2BI7Bcpo4B&ts=1704614400000&msg_id=123456&msg_type=merchant_refund
Content-Type: application/json
{
"order_id": "20250107123456789",
"refund_amount": 9900,
"refund_time": 1704614400000,
"reason": "用户申请退款"
}
1.2 开发者接口响应约定
开发者服务器收到消息后,必须返回以下格式的JSON响应:
{
"code": 0,
"message": "success",
"data": null
}
响应字段说明:
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| code | Integer | 是 | 响应码,必须返回0表示成功,其他值表示失败 |
| message | String | 是 | 响应消息 |
| data | Object | 否 | 响应数据,可为null |
- 只有当返回的
code等于0时,平台才认为消息推送成功 - 如果返回其他code、请求超时或网络异常,平台会进行自动重试
- 建议开发者服务器在5秒内完成响应
- 开发者应根据
msg_id进行消息去重,避免重复处理
2 签名计算准则
2.1 签名算法
本系统采用 HMAC-SHA256 签名算法,这是一种结合了SHA-256哈希函数和密钥的消息认证码算法,用于验证数据的完整性和真实性。
参考:
2.2 签名参数
参与签名的参数
- ts:时间戳(从URL参数中获取,单位毫秒)
- 消息内容:请求Body中的所有字段(JSON对象)
sign、access_key、msg_id、msg_type 这些参数本身不参与签名计算。
签名密钥
使用开发者在开放平台申请的 access_token 作为签名密钥。
2.3 签名计算示例
假设回调请求如下:
URL参数:
ts=1704614400000msg_id=123456msg_type=merchant_refund
请求Body:
{
"order_id": "20250107123456789",
"refund_amount": 9900,
"refund_time": 1704614400000
}
已申请的 access_token: your_access_token_here_32_characters
计算过程如下:
(1) 将每个参数拼接为 name=value 的结构
ts=1704614400000
order_id=20250107123456789
refund_amount=9900
refund_time=1704614400000
- 字符串类型:直接使用值
- 数字类型:转换为字符串
- 布尔类型:使用小写的
true或false - 数组/List类型:将元素用逗号
,连接成字符串(如:102,103,89) - 空值或空字符串:不参与签名
(2) 按参数名字典序升序排序
order_id=20250107123456789
refund_amount=9900
refund_time=1704614400000
ts=1704614400000
(3) 使用 & 字符拼接参数对
order_id=20250107123456789&refund_amount=9900&refund_time=1704614400000&ts=1704614400000
(4) 使用 access_token 计算 HMAC-SHA256 签名
- 使用
access_token=your_access_token_here_32_characters作为密钥创建 HMAC-SHA256 算法实例 - 对步骤(3)中的字符串进行HMAC-SHA256散列计算
- 将散列结果进行Base64编码
- 将Base64结果中的URL不安全字符
+、/、=替换为B
最终得到签名:sign=WbGNoWSnhogpKzilnQfPciPYdJgiTc2w6T2BI7Bcpo4B
2.4 签名验证代码参考
Java 示例代码
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class SignatureValidator {
private static final String ALGORITHM = "HmacSHA256";
public static boolean validateSign(String msgContent, long ts, String sign, String accessToken) {
try {
Map<String, Object> msgMap = parseJson(msgContent);
List<String> paramPairs = new ArrayList<>();
paramPairs.add("ts=" + ts);
for (Map.Entry<String, Object> entry : msgMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (value != null && !value.toString().isEmpty()) {
paramPairs.add(key + "=" + value.toString());
}
}
Collections.sort(paramPairs);
String data = String.join("&", paramPairs);
String computedSign = computeSign(data, accessToken);
return sign.equals(computedSign);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
private static String computeSign(String data, String secretKey) throws Exception {
Mac mac = Mac.getInstance(ALGORITHM);
SecretKeySpec signingKey = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8), ALGORITHM
);
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
String rawSign = Base64.getEncoder().encodeToString(rawHmac);
return rawSign.replaceAll("[+/=]", "B");
}
}
PHP 示例代码
<?php
class SignatureValidator {
private const ALGORITHM = 'sha256';
public static function validateSign($msgContent, $ts, $sign, $accessToken) {
try {
$msgArray = json_decode($msgContent, true);
if ($msgArray === null) return false;
$paramPairs = [];
$paramPairs[] = 'ts=' . $ts;
foreach ($msgArray as $key => $value) {
if ($value !== null && $value !== '') {
$paramPairs[] = $key . '=' . $value;
}
}
sort($paramPairs);
$data = implode('&', $paramPairs);
$computedSign = self::computeSign($data, $accessToken);
return $sign === $computedSign;
} catch (Exception $e) {
return false;
}
}
private static function computeSign($data, $secretKey) {
$rawHmac = hash_hmac(self::ALGORITHM, $data, $secretKey, true);
$rawSign = base64_encode($rawHmac);
return str_replace(['+', '/', '='], 'B', $rawSign);
}
}
?>
Python 示例代码
import hmac
import hashlib
import base64
import json
class SignatureValidator:
ALGORITHM = 'sha256'
@staticmethod
def validate_sign(msg_content: str, ts: int, sign: str, access_token: str) -> bool:
try:
msg_dict = json.loads(msg_content)
param_pairs = [f'ts={ts}']
for key, value in msg_dict.items():
if value is not None and value != '':
param_pairs.append(f'{key}={value}')
param_pairs.sort()
data = '&'.join(param_pairs)
computed_sign = SignatureValidator.compute_sign(data, access_token)
return sign == computed_sign
except Exception as e:
return False
@staticmethod
def compute_sign(data: str, secret_key: str) -> str:
raw_hmac = hmac.new(
secret_key.encode('utf-8'),
data.encode('utf-8'),
hashlib.sha256
).digest()
raw_sign = base64.b64encode(raw_hmac).decode('utf-8')
return raw_sign.replace('+', 'B').replace('/', 'B').replace('=', 'B')
Node.js 示例代码
const crypto = require('crypto');
class SignatureValidator {
static ALGORITHM = 'sha256';
static validateSign(msgContent, ts, sign, accessToken) {
try {
const msgObj = JSON.parse(msgContent);
const paramPairs = [`ts=${ts}`];
for (const [key, value] of Object.entries(msgObj)) {
if (value !== null && value !== undefined && value !== '') {
paramPairs.push(`${key}=${value}`);
}
}
paramPairs.sort();
const data = paramPairs.join('&');
const computedSign = this.computeSign(data, accessToken);
return sign === computedSign;
} catch (e) {
return false;
}
}
static computeSign(data, secretKey) {
const hmac = crypto.createHmac(this.ALGORITHM, secretKey);
hmac.update(data, 'utf8');
const rawHmac = hmac.digest();
const rawSign = rawHmac.toString('base64');
return rawSign.replace(/[+/=]/g, 'B');
}
}
3 添加订阅时的测试回调
3.1 测试回调说明
当开发者在平台添加新的消息订阅时,平台会自动发送一条测试消息到配置的回调地址,验证回调地址是否可用以及开发者是否正确实现了签名验证逻辑。
3.2 测试消息格式
平台发送的测试消息格式如下:
URL参数:
{notify_url}?sign={sign}&ts={ts}&msg_id={msg_id}&msg_type=test
消息类型: test(固定值)
请求Body:
{
"appId": "bili123456789"
}
完整请求示例:
POST https://your-domain.com/callback?sign=xxx&ts=1704614400000&msg_id=123456&msg_type=test
Content-Type: application/json
{
"appId": "bili123456789"
}
3.3 签名计算示例
假设测试消息参数如下:
app_id:bili123456789ts:1704614400000access_token:your_access_token_here_32_characters
参与签名的参数:
appId=bili123456789
ts=1704614400000
按字典序排序并拼接:
appId=bili123456789&ts=1704614400000
使用 access_token 计算 HMAC-SHA256 签名,得到 sign 值。
3.4 开发者服务器响应要求
开发者服务器收到测试消息后,必须:
- 验证签名:使用第2节中的方法验证签名是否正确
- 返回成功响应:必须返回以下格式的JSON
{
"code": 0,
"message": "success",
"data": null
}
重要: 只有返回 code=0 时,平台才认为测试成功。
签名计算工具
在这里输入参数,自动计算签名,帮助您快速验证签名逻辑是否正确。