# 小程序消息订阅推送流程
小程序订阅消息是小程序能力中的重要组成部分,通过该功能,开发者可以在获得用户授权后,向用户发送服务通知,实现服务的闭环和提供更优质的用户体验。订阅消息的实现方式由 APP 自定义,可以实现类似站内信、通知栏等。
本文档将从以下三个部分说明小程序消息订阅推送的完整流程:
- 管理后台消息模板和渠道的配置:介绍开发者如何配置消息模板、配置应用消息渠道、关联消息模板和渠道、配置应用Webhook
- 用户授权流程:说明用户如何授权接收消息以及授权信息的处理过程
- 消息推送流程:详述从消息触发到发送、接收和展示的完整过程
# 1. 管理后台消息模板和渠道的配置
# 1.1 配置应用的消息渠道
说明 应用的消息渠道是由应用方自行维护的配置,属于应用维度的设置。平台侧不限定也无法限定应用的消息渠道实现方式,应用方可根据自身业务需求进行灵活配置。应用可以根据自身需求设置多种消息渠道,如站内信、短信、邮件等,以满足不同场景下的消息推送需求。
入口
开发中心-「应用」-「消息渠道」-「消息渠道配置」
新增消息渠道
# 1.2 配置应用的消息推送 Webhook
说明 Webhook 是平台向应用推送消息的目标地址。在消息推送过程中,平台会将消息内容通过 HTTP POST 请求发送到应用配置的 Webhook 地址。由于平台侧不限定应用的消息推送实现方式,因此通过 Webhook 机制实现消息的灵活推送,应用方可自行处理接收到的消息并进行后续业务处理,如用户通知等。
入口
开发中心-「应用」-「消息渠道」-「消息推送 Webhook 配置」
配置消息推送的 Webhook
# 1.3 创建小程序消息模板
说明 小程序消息模板的创建需要经过「数字中心」的审核流程。开发者可在管理后台创建消息模板,定义消息的内容格式和变量,提交后由数字中心进行审核,确保消息内容符合规范要求。审核通过后的消息模板才能用于实际的消息推送,这一机制有助于保障消息内容的合规性和用户体验。
入口
开发中心-「小程序」-「消息模板」-「申请创建」
申请消息模板
# 1.4 消息模板申请关联应用的消息渠道
说明 消息模板需要与应用的消息渠道关联才能实现消息推送。当消息模板申请关联应用的渠道时,需要应用方进行审核确认。这一步骤确保了消息模板与应用渠道的合理匹配,应用方可根据自身业务需求决定是否批准关联请求。关联成功后,平台可通过该渠道向用户推送基于此模板的消息。
入口
开发中心-「小程序」-「关联应用」-「消息渠道关联」
申请关联渠道
# 2. 用户订阅消息流程
# 2.1 小程序服务端调用 OpenAPI 获取可用的模板列表
接口详见: 开发中心 OpenAPI - 获取小程序可用的消息模板 (opens new window)
# 2.2 用户授权请求
- 触发授权请求:用户在使用小程序相关功能时,小程序客户端可以通过
wx.requestSubscribeMessage({tmplIds: [...]})
接口触发授权请求 - 展示授权界面:小程序展示授权请求界面,向用户说明授权的目的和内容。
# 2.3 授权处理
- 用户操作:用户可以选择同意或拒绝授权请求,授权选择会通过 FinClip SDK 记录在 FinClip 服务端中。如果用户同意授权,系统会记录用户的授权信息,并允许向该用户发送消息。
# 3. 消息推送流程
# 3.1 消息触发
当特定业务事件发生时(如订单状态变更、活动提醒等),「小程序服务端」可以主动选择触发消息推送。 调用如下的 OpenAPI 接口进行消息的推送:
开发中心 OpenAPI - 推送小程序消息模板 (opens new window)
# 4. 其他
# 4.1 应用的消息推送 Webhook 的 body 长什么样?
webhook 的推送 body 示例如下
{
"miniAppId": "fc1111", // 小程序 id
"userId": "zhangsan", // userId,和 sdk 初始化的时候传入的 userId 一致
"templates": [
{
"messageDeliveryTemplateId": "2696141363995781", // 消息模板 id
"text": "your order has been shipped, express number is 123456", // 消息内容
"link": "/order/orderDetail", // 消息跳转链接
"desc": "this is a description", // 消息描述
"channelCodes": [
"notificationCenter" // 消息渠道代码
]
}
]
}
# 4.2 应用的消息推送 Webhook 如何保障传输来源的可信?
为了使得您的服务 (webhook事件的接收方) 有途径校验请求来自可信任的调用方,提供了签名的方式。为此您需要:
- 在配置Webhook的时候,手动填写 Secret
- Webhook接收方中校验:Secret+请求的入参body 计算出来的值需要等于 header中的
X-Fc-Webhook-Sign
- 配置Webhook的时候,如果没有填写 Secret, 不会传
X-Fc-Webhook-Sign
的header
- 配置Webhook的时候,如果没有填写 Secret, 不会传
# 签名方式说明
X-Fc-Webhook-Sign
的签名使用了请求body的 HMAC 十六进制摘要,并使用 SHA-256 哈希函数生成,并将 作为webhookToken
HMAC key
验证 webhook 有效负载时需要牢记以下几件重要的事情:
- FinClip 使用 HMAC 十六进制摘要来计算哈希值。
- 哈希签名始终以 开头
sha256=
。 - 哈希签名是使用您的 webhook 的 webhookToken 和 webhook请求体内容 生成的。
- 如果您的语言和服务器实现指定了字符编码,请确保将负载处理为 UTF-8。Webhook 负载可以包含 Unicode 字符。
# 签名示例
Java
您可以定义以下verifySignature
方法并在收到 webhook 请求时调用它:
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class WebhookVerifier {
/**
* 验证webhook签名
*
* @param payload webhook请求的request body
* @param webhookToken 生成的webhookToken
* @param signature webhook请求中header中的 X-Fc-Webhook-Sign
* @return 签名是否有效
*/
public static boolean verifySignature(String payload, String webhookToken, String signature) {
String expectedSignature = computeSign(payload, webhookToken);
return signature.equals(expectedSignature);
}
/**
* 计算签名
*
* @param payload webhook请求的request body
* @param webhookToken 生成的webhookToken
* @return 签名
*/
public static String computeSign(String payload, String webhookToken) {
try {
String algorithm = "HmacSHA256";
Mac mac = Mac.getInstance(algorithm);
SecretKeySpec keySpec = new SecretKeySpec(webhookToken.getBytes(StandardCharsets.UTF_8), algorithm);
mac.init(keySpec);
// 计算签名
byte[] signatureBytes = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
// 转换为16进制字符串
String hexDigest = bytesToHex(signatureBytes);
return "sha256=" + hexDigest;
} catch (Exception e) {
throw new RuntimeException("err", e);
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}
Golang
您可以定义以下VerifySignature
函数并在收到 webhook 请求时调用它:
// 参数说明
// payload: webhook请求的request body
// webHookToken: 生成的webhookToken
// signature: webhook请求中header中的 X-Fc-Webhook-Sign
func VerifySignature(payload []byte, webHookToken string, signature string) bool {
expectedSignature := computeWebHookSign(payload, webHookToken)
return hmac.Equal([]byte(expectedSignature), []byte(signature))
}
func computeWebHookSign(payload []byte, webhookToken string) string {
mac := hmac.New(sha256.New, []byte(webhookToken))
mac.Write(payload)
return "sha256=" + hex.EncodeToString(mac.Sum(nil))
}
Python
您可以定义以下verify_signature
函数并在收到 webhook 请求时调用它:
import hashlib
import hmac
def verify_signature(payload_body, secret_token, signature_header):
"""Verify that the payload was sent from GitHub by validating SHA256.
Raise and return 403 if not authorized.
Args:
payload_body: original request body to verify (request.body())
secret_token: GitHub app webhook token (WEBHOOK_SECRET)
signature_header: header received from GitHub (x-hub-signature-256)
"""
if not signature_header:
raise HTTPException(status_code=403, detail="x-hub-signature-256 header is missing!")
hash_object = hmac.new(secret_token.encode('utf-8'), msg=payload_body, digestmod=hashlib.sha256)
expected_signature = "sha256=" + hash_object.hexdigest()
if not hmac.compare_digest(expected_signature, signature_header):
raise HTTPException(status_code=403, detail="Request signatures didn't match!")
JavaScript
let encoder = new TextEncoder();
async function verifySignature(secret, header, payload) {
let parts = header.split("=");
let sigHex = parts[1];
let algorithm = { name: "HMAC", hash: { name: 'SHA-256' } };
let keyBytes = encoder.encode(secret);
let extractable = false;
let key = await crypto.subtle.importKey(
"raw",
keyBytes,
algorithm,
extractable,
[ "sign", "verify" ],
);
let sigBytes = hexToBytes(sigHex);
let dataBytes = encoder.encode(payload);
let equal = await crypto.subtle.verify(
algorithm.name,
key,
sigBytes,
dataBytes,
);
return equal;
}
function hexToBytes(hex) {
let len = hex.length / 2;
let bytes = new Uint8Array(len);
let index = 0;
for (let i = 0; i < hex.length; i += 2) {
let c = hex.slice(i, i + 2);
let b = parseInt(c, 16);
bytes[index] = b;
index += 1;
}
return bytes;
}