接入准备
一、开发者接入准备
1.1 创建小程序
需要提前在字节小程序开放平台创建小程序,获取小程序 appid,此时只是完成了小程序的命名;
操作指引:https://microapp.bytedance.com/
1.2 完成小程序主体认证
创建小程序后,需要完成小程序主体认证,主体认证逻辑详见文档;
1.3 完善并配置小程序基础信息
完善小程序基础信息,包括「小程序简介」、「小程序头像」、「小程序图标」、「服务类目」,关键字搜索配置等;
1.4 加签验签开发
- 完成进件后,开发者可在字节开放平台-【某小程序】-【功能】-【支付】-【支付产品】-【支付设置】中查看支付系统秘钥 SALT;

- 调用支付相关接口需要带上签名参数,推荐使用小程序开发者工具对加签逻辑进行校验,详细加签规则参考下方 DEMO;
- 构造交易数据并签名必须在开发者服务端完成,应用私钥绝对不能保存在开发者客户端/前端中,也不能从开发者服务端下发。
二、服务商接入准备
2.1 创建服务商应用
服务商需要在【字节小程序开放平台-第三方平台】创建应用,这个条件是服务商完成后续工作的前提;
操作指引:https://microapp.bytedance.com/docs/zh-CN/mini-app/thirdparty/overview/create/
2.2 完成小程序授权
小程序拥有者确认授权后,服务商才能代为开发管理;
操作指引:
https://microapp.bytedance.com/docs/zh-CN/mini-app/thirdparty/overview-guide/authorization
https://microapp.bytedance.com/docs/zh-CN/mini-app/thirdparty/overview-guide/encryption
2.3 为授权小程序开发并部署代码包
服务商需要为授权小程序开发代码并进行部署;
操作指引:https://microapp.bytedance.com/docs/zh-CN/mini-app/thirdparty/overview-guide/development-process
2.4 加签验签开发
- 服务商为自己进件成功后,可以在第三方平台-【某第三方平台】-【设置】-【开发设置】中查看分配的秘钥;
- 调用支付相关接口需要带上签名参数,推荐使用小程序开发者工具对加签逻辑进行校验,详细加签规则参考下方 DEMO;
- 构造交易数据并签名必须在服务商的服务端完成,应用私钥绝对不能保存在服务商客户端/前端中,也不能从服务商服务端下发。
三、签名 DEMO
3.1 DEMO 示例
为方便开发者快速接入,我们提供了 Java 和 Go 语言对应的签名 DEMO 示例供参考。
- open-pay-signature-java-demo,适用于Java开发者
- open-pay-signature-go-demo,适用于Go开发者
3.2 回调签名算法
支付回调通知开发者服务端时,会使用如下的算法进行签名,供开发者验证请求的来源:
- 将所有字段(验证时注意不包含 sign 签名本身,不包含空字段与 type 常量字段)内容与平台上配置的 token 一起,按照字典序排序
- 所有字段内容连接成一个字符串
- 使用 sha-1 算法计算字符串摘要作为签名
上述步骤计算出的签名 signature,和支付回调请求体里面的 signature 对比,如果不一致,说明请求不可信任(如被篡改)。
java(jdk 1.8)示例
List<String> sortedString = Arrays.asList("", "", "", "", token, timestamp, nonce, msg);
String concat = sortedString.stream().sorted().collect(Collectors.joining(""));
byte[] arrayByte = concat.getBytes(StandardCharsets.UTF_8);
MessageDigest mDigest = MessageDigest.getInstance("SHA1");
byte[] digestByte = mDigest.digest(arrayByte);
StringBuffer signBuilder = new StringBuffer();
for (byte b : digestByte) {
signBuilder.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
}
if (Signature.equals(signBuilder.toString())) {
return echostr;
}
return "";
golang 示例
sortedString := make([]string, 0)
sortedString = append(sortedString, token)
sortedString = append(sortedString, timestamp)
sortedString = append(sortedString, nonce)
sortedString = append(sortedString, msg)
sort.Strings(sortedString)
h := sha1.New()
h.Write([]byte(strings.Join(sortedString, "")))
bs := h.Sum(nil)
_signature := fmt.Sprintf("%x", bs)
nodejs 示例(Koa)
// http handler GET
function handler(ctx) {
const { signature, timestamp, msg: '', nonce, echostr } = ctx.query;
const strArr = [token, timestamp, nonce, msg].sort();
const str = strArr.join('');
const _signature = require('crypto').createHash('sha1').update(str).digest('hex');
}
3.3 回调响应
在开发者服务端收到回调且处理成功后,需要按以下 json 返回表示处理成功,否则会认为通知失败进行重试。
{
"err_no": 0,
"err_tips": "success"
}
3.4 请求签名算法
发往小程序服务端的请求,在没有特殊说明时,均需要使用担保支付秘钥进行签名,用于保证请求的来源:
- sign, app_id , thirdparty_id 字段用于标识身份字段,不参与签名。将其他字段内容(不包含 key)与支付 SALT 一起进行字典序排序后,使用&符号链接
- 使用 md5 算法对该字符串计算摘要,作为结果
- 参与加签的字段均以 POST 请求中的 body 内容为准, 不考虑参数默认值等规则. 对于对象类型与数组类型的参数, 使用 POST 中的字符串原串进行左右去除空格后进行加签
- 如有其他安全性需要, 可以在请求中添加 nonce 字段, 该字段无任何业务影响, 仅影响加签内容, 使同一请求的多次签名不同.
php 示例加签
<?php
function sign($map) {
$rList = array();
foreach($map as $k =>$v) {
if ($k == "other_settle_params" || $k == "app_id" || $k == "sign" || $k == "thirdparty_id")
continue;
$value = trim(strval($v));
$len = strlen($value);
if ($len > 1 && substr($value, 0,1)=="\"" && substr($value,$len, $len-1)=="\"")
$value = substr($value,1, $len-1);
$value = trim($value);
if ($value == "" || $value == "null")
continue;
array_push($rList, $value);
}
array_push($rList, "your_payment_salt");
sort($rList, 2);
return md5(implode('&', $rList));
}
?>
golang 示例加签
import (
"crypto/md5"
"fmt"
"sort"
"strings"
)
// 支付密钥值
const salt = "your_payment_salt"
/*
paramsMap: POST 请求中的字符串转换为 map
eg:
"{\"a\":\"string\",\"b\":1,\"c\":true}"
==>
map[string]interface {}{"a":"string", "b":1, "c":true}
*/
func getSign(paramsMap map[string]interface{}) string {
var paramsArr []string
for k, v := range paramsMap {
if k == "other_settle_params" {
continue
}
value := strings.TrimSpace(fmt.Sprintf("%v", v))
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") && len(value) > 1 {
value = value[1 : len(value)-1]
}
value = strings.TrimSpace(value)
if value == "" || value == "null" {
continue
}
switch k {
// app_id, thirdparty_id, sign 字段用于标识身份,不参与签名
case "app_id", "thirdparty_id", "sign":
default:
paramsArr = append(paramsArr, value)
}
}
paramsArr = append(paramsArr, salt)
sort.Strings(paramsArr)
return fmt.Sprintf("%x", md5.Sum([]byte(strings.Join(paramsArr, "&"))))
}
nodejs 示例(Koa)
// params 代表请求内容
var skip_arr = ["thirdparty_id", "app_id", "sign"];
var paramArray = new Array();
for (var k in params) {
if (skip_arr.indexOf(k) != -1) {
continue;
}
if (params[k] == "") {
continue;
}
paramArray.push(params[k]);
}
paramArray.push(SALT);
paramArray.sort();
var signStr = paramArray.join("&");
return md5(signStr);
Java(jdk 1.8) 示例加签
// 支付密钥值
private static final String SALT = "your_payment_salt";
// paramsMap 参数含义解释同 golang
public static String getSign(Map<String, Object> paramsMap) {
List<String> paramsArr = new ArrayList<>();
for (Map.Entry<String, Object> entry : paramsMap.entrySet()) {
String key = entry.getKey();
if (key.equals("other_settle_params")) {
continue;
}
String value = entry.getValue().toString();
value = value.trim();
if (value.startsWith("\"") && value.endsWith("\"") && value.length() > 1) {
value = value.substring(1, value.length() - 1);
}
value = value.trim();
if (value.equals("") || value.equals("null")) {
continue;
}
switch (key) {
case "app_id":
case "thirdparty_id":
case "sign":
break;
default:
paramsArr.add(value);
break;
}
}
paramsArr.add(SALT);
Collections.sort(paramsArr);
StringBuilder signStr = new StringBuilder();
String sep = "";
for (String s : paramsArr) {
signStr.append(sep).append(s);
sep = "&";
}
return md5FromStr(signStr.toString());
}
public static String md5FromStr(String inStr) {
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return "";
}
byte[] byteArray = inStr.getBytes(StandardCharsets.UTF_8);
byte[] md5Bytes = md5.digest(byteArray);
StringBuilder hexValue = new StringBuilder();
for (byte md5Byte : md5Bytes) {
int val = ((int) md5Byte) & 0xff;
if (val < 16) {
hexValue.append("0");
}
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
python 示例加签(python2 和 python3 注意 get_sign 返回值的处理!!!)
# -*- coding: UTF-8 -*-
import hashlib
# 支付密钥值
SALT = "your_payment_salt"
# params_map 参数含义解释同 golang
def get_sign(params_map): # type: (dict[str, any]) -> str
params_list = []
for k, v in params_map.items():
if k == "other_settle_params":
continue
value = str(v).strip()
if value.startswith("\"") and value.endswith("\"") and len(value) > 1:
value = value[1: len(value) - 1]
value = value.strip()
if value == "" or value == "null":
continue
if k not in ("app_id", "thirdparty_id", "sign"):
params_list.append(value)
params_list.append(SALT)
params_list.sort()
original_str = str("&").join(params_list)
# python2
return hashlib.md5(original_str).hexdigest()
# python3
# return hashlib.md5(original_str.encode("utf-8")).hexdigest()
.net C# 示例加签
// 支付密钥值
private const string Salt = "your_payment_salt";
private static string GetSign(Dictionary<string, Object> paramsDict)
{
var paramsList = new ArrayList();
foreach (var (k, v) in paramsDict)
{
if ("other_settle_params".Equals(k))
{
continue;
}
var value = v.ToString().Trim();
if (value.StartWith("\"") && value.EndsWith("\"") && value.Length > 1)
{
value = value.Substring(1, value.Length - 1);
}
value = value.Trim();
if ("".Equals(value) || "null".Equals(value))
{
continue;
}
switch (k)
{
case "app_id":
case "thirdparty_id":
case "sign":
break;
default:
paramsList.Add(value);
break;
}
}
paramsList.Add(Salt);
// 按照 ASCII 编码进行排序
var paramsArray = (string[]) paramsList.ToArray(typeof(string));
Array.Sort(paramsArray, string.CompareOrdinal);
var signStr = string.Join("&", paramsArray);
return GetMd5(signStr);
}
private static string GetMd5(string sDataIn)
{
var md5 = new MD5CryptoServiceProvider();
byte[] bytValue, bytHash;
bytValue = Encoding.UTF8.GetBytes(sDataIn);
bytHash = md5.ComputeHash(bytValue);
md5.Clear();
var sTemp = bytHash.Aggregate("", (current, t) => current + t.ToString("X").PadLeft(2, '0'));
return sTemp.ToLower();
}