接入准备
收藏
我的收藏

开发者接入准备​

第一步:入驻开发者平台​

开发者需要完成抖音开发平台的入驻才能获取相应服务。​
操作指引:入驻开发者平台

第二步:创建小程序​

需要提前在抖音开放平台创建小程序,获取小程序 appid,此时只是完成了小程序的命名;​
操作指引:抖音开放平台

第三步:完成小程序主体认证​

创建小程序后,需要完成小程序主体认证。​

第四步:完善并配置小程序基础信息​

完善小程序基础信息,包括「小程序简介」、「小程序头像」、「小程序图标」、「服务类目」,关键字搜索配置等。​

第五步:加签验签开发​

    1.如果是非生活服务行业的小程序,需要先在抖音开放平台-【某小程序】-【能力】-【支付】-【支付能力】选择支付解决方案,选择支付解决方案之后可以操作进件。如果是生活服务行业的小程序,不需要此步骤。
    2.完成进件后,开发者可在抖音开放平台-【某小程序】-【能力】-【支付】-【支付方式管理】-【支付设置】中查看支付系统密钥 SALT。
    3.DEMO 示例
    4.构造交易数据并签名必须在开发者服务端完成,应用私钥绝对不能保存在开发者客户端/前端中,也不能从开发者服务端下发。
    5.推荐使用签名校验工具,辅助验证生产签名的准确性。

服务商接入准备​

第一步:入驻服务商平台​

服务商需要先完成抖音平台的入驻才能获取相应服务。​
操作指南:入驻服务商平台

第二步:创建服务商应用​

服务商需要在【抖音开放平台-服务商平台】创建第三方应用,这个条件是服务商完成后续工作的前提。​

第三步:完成小程序授权​

小程序拥有者确认授权消息加解密后,服务商才能代为开发管理。​

第四步:为授权小程序开发并部署代码包​

服务商需要为授权小程序开发代码并进行部署,详见开发流程。​

第五步:加签验签开发​

    1.服务商通过接口获取saas页面为自己进件成功后,可以在第三方平台-【某第三方平台】-【设置】-【开发设置】中查看分配的密钥。
    2.DEMO 示例
    3.构造交易数据并签名必须在服务商的服务端完成,应用私钥绝对不能保存在服务商客户端/前端中,也不能从服务商服务端下发。

签名 DEMO​

DEMO 示例​

为方便开发者快速接入,我们提供了 Java 和 Go 语言对应的签名 DEMO 示例供参考。​

回调签名算法​

支付回调通知开发者服务端时,会使用如下的算法进行签名,供开发者验证请求的来源:​
    1.在抖音开放平台-【某小程序】-【功能】-【支付】-【支付产品】-【支付设置】中获取Token, 按照接口文档将所有请求字段(验证时注意不包含签名本身,不包含空字段与回调类型标记 type 常量字段)内容与平台上配置的 token 一起,进行字典序排序。
    2.所有字段内容连接成一个字符串
    3.使用 sha-1 算法计算字符串摘要作为签名
上述步骤计算出的签名 signature,和支付回调请求体里面的 signature 对比,如果不一致,说明请求不可信任(如被篡改)。​

请求示例​

支付结果回调接口为例,小程序的token值为token,请求回调参数如下​
Plain Text
复制
{
"timestamp": "timestamp",
"nonce": "nonce",
"msg": "msg",
"msg_signature": "msg_signature",
"type": "payment"
}

Java(JDK 1.8)示例

js
复制
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));
}
String signature = signBuilder.toString();

Golang 示例

Go
复制
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)

Node.js 示例(Koa)

JavaScript
复制
// http handler GET
function handler(ctx) {
const {timestamp, msg, nonce} = ctx.body;
const strArr = [token, timestamp, nonce, msg].sort();
const str = strArr.join('');
const _signature = require('crypto').createHash('sha1').update(str).digest('hex');
}

回调响应​

在开发者服务端收到回调且处理成功后,需要按以下 json 返回表示处理成功,否则会认为通知失败进行重试。​
json
复制
{
"err_no": 0,
"err_tips": "success"
}

请求签名算法​

发往小程序服务端的请求,在没有特殊说明时,均需要使用担保支付密钥进行签名,用于保证请求的来源:​
    1.sign, app_id , thirdparty_id , prod_id 字段用于标识身份字段,不参与签名。将其他请求参数的字段内容(不包含请求参数的key)与支付 SALT 一起进行字典序排序后,使用&符号链接。
    2.使用 md5 算法对该字符串计算摘要,作为结果。
    3.部分请求字段非必填或条件选填,在值为空的情况下不需要参与签名;参与加签的字段均以 POST 请求中的 body 内容为准, 不考虑参数默认值等规则. 对于对象类型与数组类型的参数, 使用 POST 中的字符串原串进行左右去除空格后进行加签。
    4.如有其他安全性需要, 可以在请求中添加 nonce 字段, 该字段无任何业务影响, 仅影响加签内容, 使同一请求的多次签名不同。
    5.推荐使用签名校验工具,辅助验证生产签名的准确性。​

PHP 示例加签

PHP
复制
<?php
function sign($map) {
$rList = [];
foreach($map as $k =>$v) {
if ($k == "other_settle_params" || $k == "app_id" || $k == "sign" || $k == "thirdparty_id")
continue;
$value = trim(strval($v));
if (is_array($v)) {
$value = arrayToStr($v);
}
$len = strlen($value);
if ($len > 1 && substr($value, 0,1)=="\"" && substr($value, $len-1)=="\"")
$value = substr($value,1, $len-1);
$value = trim($value);
if ($value == "" || $value == "null")
continue;
$rList[] = $value;
}
$rList[] = "your_payment_salt";
sort($rList, SORT_STRING);
return md5(implode('&', $rList));
}
function arrayToStr($map) {
$isMap = isArrMap($map);
$result = "";
if ($isMap){
$result = "map[";
}
$keyArr = array_keys($map);
if ($isMap) {
sort($keyArr);
}
$paramsArr = array();
foreach($keyArr as $k) {
$v = $map[$k];
if ($isMap) {
if (is_array($v)) {
$paramsArr[] = sprintf("%s:%s", $k, arrayToStr($v));
} else {
$paramsArr[] = sprintf("%s:%s", $k, trim(strval($v)));
}
} else {
if (is_array($v)) {
$paramsArr[] = arrayToStr($v);
} else {
$paramsArr[] = trim(strval($v));
}
}
}
$result = sprintf("%s%s", $result, join(" ", $paramsArr));
if (!$isMap) {
$result = sprintf("[%s]", $result);
} else {
$result = sprintf("%s]", $result);
}
return $result;
}
function isArrMap($map) {
foreach($map as $k =>$v) {
if (is_string($k)){
return true;
}
}
return false;
}
?>

Golang 示例加签

Go
复制
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, "&"))))
}

Java(JDK 1.8)示例加签

Java
复制
public static String Map2String(Map<String, Object> paramsMap) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("map[");
List<String> params = new ArrayList<>();
List<String> paramsArr = new ArrayList<>();
for (String key : paramsMap.keySet()) {
paramsArr.add(key);
}
Collections.sort(paramsArr);
for (String key : paramsArr) {
Object objValue = paramsMap.get(key);
if (objValue instanceof Map) {
params.add(key + ":" + Map2String((Map) objValue));
} else if (objValue instanceof List) {
params.add(key + ":" + List2String((List) objValue));
} else {
params.add(key + ":" + objValue.toString());
}
}
stringBuilder.append(String.join(" ", params));
stringBuilder.append("]");
return stringBuilder.toString();
}
public static String List2String(List<Object> list) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("[");
List<String> paramsArr = new ArrayList<>();
for (Object item : list) {
if (item instanceof List) {
paramsArr.add(List2String((List) item));
} else if (item instanceof Map) {
paramsArr.add(Map2String((Map) item));
} else {
paramsArr.add(item.toString());
}
}
stringBuilder.append(String.join(" ", paramsArr));
stringBuilder.append("]");
return stringBuilder.toString();
}
public final static List<String> REQUEST_NOT_NEED_SIGN_PARAMS = Arrays.asList("app_id", "thirdparty_id", "sign", "other_settle_params");
public static String requestSign(Map<String, Object> paramsMap) {
List<String> paramsArr = new ArrayList<>();
for (Map.Entry<String, Object> entry : paramsMap.entrySet()) {
String key = entry.getKey();
if (REQUEST_NOT_NEED_SIGN_PARAMS.contains(key)) {
continue;
}
Object objValue = entry.getValue();
String value = "";
if (objValue instanceof Map) {
value = Map2String((Map) objValue);
} else if (objValue instanceof List) {
value = List2String((List) objValue);
} else {
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;
}
paramsArr.add(value);
}
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());
}
private 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();
}