接入准备
收藏
我的收藏

开发者接入准备​

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

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

第二步:创建小程序​

需要提前在抖音开放平台创建小程序,获取小程序 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,请求回调参数如下​
{ "timestamp": "timestamp", "nonce": "nonce", "msg": "msg", "msg_signature": "msg_signature", "type": "payment" }

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

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)

Node.js 示例(Koa)

// 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 返回表示处理成功,否则会认为通知失败进行重试。​
{ "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 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 示例加签

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)示例加签

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(); }