消息订阅开发者文档

概述

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 签名参数

参与签名的参数

  1. ts:时间戳(从URL参数中获取,单位毫秒)
  2. 消息内容:请求Body中的所有字段(JSON对象)
注意: signaccess_keymsg_idmsg_type 这些参数本身不参与签名计算

签名密钥

使用开发者在开放平台申请的 access_token 作为签名密钥。

2.3 签名计算示例

假设回调请求如下:

URL参数:

  • ts=1704614400000
  • msg_id=123456
  • msg_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
参数处理规则:
  • 字符串类型:直接使用值
  • 数字类型:转换为字符串
  • 布尔类型:使用小写的 truefalse
  • 数组/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 签名

  1. 使用 access_token=your_access_token_here_32_characters 作为密钥创建 HMAC-SHA256 算法实例
  2. 对步骤(3)中的字符串进行HMAC-SHA256散列计算
  3. 将散列结果进行Base64编码
  4. 将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: bili123456789
  • ts: 1704614400000
  • access_token: your_access_token_here_32_characters

参与签名的参数:

appId=bili123456789
ts=1704614400000

按字典序排序并拼接:

appId=bili123456789&ts=1704614400000

使用 access_token 计算 HMAC-SHA256 签名,得到 sign 值。

3.4 开发者服务器响应要求

开发者服务器收到测试消息后,必须:

  1. 验证签名:使用第2节中的方法验证签名是否正确
  2. 返回成功响应:必须返回以下格式的JSON
{
  "code": 0,
  "message": "success",
  "data": null
}

重要: 只有返回 code=0 时,平台才认为测试成功。


签名计算工具

在这里输入参数,自动计算签名,帮助您快速验证签名逻辑是否正确。

您在开放平台申请的访问密钥
Unix时间戳,单位毫秒
请输入有效的JSON格式数据