第三方小程序应用密钥使用说明

收藏
我的收藏
针对开放平台对外开放的能力,制定了一套签名机制与规范,当且仅当鉴权认证通过之后,才允许执行相应的接口逻辑。签名算法目前应用在对安全要求非常高的场景,如涉及资金交易的场景。除此外,绝大部分其他场景不需要接入签名。

密钥介绍

鉴权认证机制采用 SHA256-RSA2048 实现。

使用说明

在鉴权认证机制中,需要 4 个密钥,分别为
    平台公钥:
    每个「第三方小程序」对应的平台公钥是不一样的,平台公钥由「开放平台」负责生成,并告知小程序代开发服务商。
    平台私钥:
    每个「第三方小程序」对应的平台私钥是不一样的,平台私钥由「开放平台」负责生成和保存。
    应用公钥:
    「第三方小程序」的应用公钥由代开发服务商生成并上传到「开放平台」,可支持更换。
    应用私钥:
    「第三方小程序」的应用私钥由代开发服务商生成并保存,不能对外提供,可支持更换。
交互方式如下图所示

应用密钥

第三方小程序应用公钥和私钥由代开发服务商生成并保存,同时第三方小程序应用公钥需要在「服务商平台」的控制台中上传。

生成方式

应用公钥和私钥的生成方式可参考:
$ openssl OpenSSL> genrsa -out private_key.pem 2048 Generating RSA private key, 2048 bit long modulus ....................+++ ...........................................................................+++ e is 65537 (0x10001) OpenSSL> rsa -in private_key.pem -pubout -out public_key.pem writing RSA key OpenSSL> exit $ ls private_key.pem public_key.pem
public_key.pem 中的整个文本数据即为需要上传的公钥,包含----BEGIN PUBLIC KEY---一直到---END PUBLIC KEY----
注意: 第三方小程序应用私钥务必妥善保存,不能对外公开。 当发现应用私钥泄漏之后,应及时重新生成,并将第三方小程序新应用公钥上传到「服务商平台」。

上传方式

    1.登录「抖音开放平台 - 服务商平台」,进入第三方小程序的「开发-开发配置」页。
    2.在「密钥设置」处点击「添加应用公钥」,将生成的应用公钥上传,上传成功后,鉴权认证机制的前期准备工作就结束了。

平台公钥
第三方小程序的平台公钥和私钥由「开放平台」负责生成和保存,不同「第三方小程序」的平台公钥和私钥是不同的。
获取方式:登录「抖音开放平台 - 服务商平台」,进入第三方小程序的「开发-开发配置」页,平台公钥在「密钥设置」处展示。

规则说明

基本信息

API 请求必须使用 HTTPS。

数据格式

消息体数据交换格式为 JSON ,请求须设置 HTTP 头部。
Content-Type: application/json Accept: application/json

参数兼容性

鉴权认证与请求参数的顺序无关。

请求的唯一标示

「开放平台」会给每一个接收到的请求分配一个唯一标示,该标示包含在应答的 HTTP 头 x-tt-logid 中。 当需要「开放平台」帮助时,请提供请求的唯一标示,以便我们更快的定位到具体的请求。

签名介绍

    签名:
    「开放平台」通过验证签名来保证请求的真实性和数据的完整性。
    请求签名:
    代开发服务商首先需要对 URL、消息体等关键数据进行拼接组合,使用应用私钥对组合后的数据进行SHA256-RSA2048签名。签名信息通过 HTTP 头Byte-Authorization传递。 「开放平台」会拒绝没有携带签名或者签名验证不通过的请求。
    应答签名:
    「开放平台」会执行签名验证成功的请求,并使用平台私钥对应答数据进行签名,签名的信息包含在 HTTP 头部。代开发服务商应拒绝没有携带签名信息的成功应答(HTTP 状态码为 2xx),应认为是被伪造的或被篡改的应答。
    回调通知签名:
    当能力接口涉及回调通知结果时,「开放平台」使用平台私钥对回调通知请求进行签名。签名的方法同应答签名的方法一致,代开发服务商务必验证回调通知请求的签名信息。

开发指南

生成签名

代开发服务商应按照下述步骤生成请求的签名信息。
第一步:构造待签名串
待签名串一共有 5 行,每一行为一个参数。行尾以 \n(换行符,ASCII 编码值为 0x0A)结束,包括最后一行。如果参数本身以\n结束,也需要附加一个\n。最终格式如下:
HTTP请求方法\n URL\n 请求时间戳\n 请求随机串\n 请求报文主体\n
其中:
    HTTP 请求方法
    如:POST,GET,PUT 等。注意请使用大写。
    URL
    获取请求的绝对 URL,并去除域名部分得到参与签名的 URL。 去除域名后的部分,必须以斜杠字符“/”开头。 如果去除域名后的部分为空,则用单个斜杠字符“/”来当作 URL 用于签名。 如果请求中有查询参数,URL 末尾应附加有'?'和对应的查询字符串。 如
PATH=https://open.douyin.com/api/trade/v2/query URL 则为 /api/trade/v2/query PATH=https://open.douyin.com/api/trade/v2/query?a=x URL 则为 /api/trade/v2/query?a=x
    请求时间戳
    发起请求时系统的当前时间戳,即格林威治时间 1970 年 01 月 01 日 00 时 00 分 00 秒(北京时间 1970 年 01 月 01 日 08 时 00 分 00 秒)起至现在的总秒数,作为请求时间戳。「开放平台」会拒绝处理一个小时之前发起的请求。
    请求随机串
    任意生成一个随机字符串,以保证相同时间相同参数发起的请求签名值不一样(我们推荐生成随机串算法如下:调用随机数函数生成,将得到的值转换为字符串)。
    请求报文主体
    获取请求中的请求报文主体(request body)。
    请求方法为GET时,报文主体为空。
    当请求方法为POST或PUT时,请使用JSON报文内容。

第二步:计算签名值

成功构造待签名串后,应使用第三方小程序应用私钥对待签名串进行SHA256-RSA2048签名,并对签名结果进行Base64编码得到签名值。
签名命令可参考:
$ echo -n -e \ "POST\n/api/business/diamond/query\n1623934869\nDC10180A100073E70A48F195DA2AF2E6\n{\"appid\":\"ttxxx\",\"order_id\":\"xxx\"}\n" \ | openssl dgst -sha256 -sign private_key.pem | openssl base64 -A
得到签名值:
nwd1L3wCX+01/TVTkILeovF1DtYeghC1VHjrcjTHVkh7+gRaONEQkC2Y72Mw8JdSnIyeAtyp/pDHzyKGywjVqv5+JOBEhQG1/pvwNHN49wD26qg3AJL4hXw0fMJSRiTQEV1MszwDLuaabvo/qM9OXL9KyYiEPwVJqYtzmho4cHXT6mYgzNOW1xt5d7RDf4QO74JI3i4dtk9Uj8svJTrrBabML6AUcqcx2OP/7xukdaUgPdPf+IqmMG6GC4n52LUDogcL5n/osLdfHg9l6kW5gDcDjBfNDaggz07QMPHGdVao7pnQ2ub7VqcFIuY6Q3cBL7ndQdDGKrv+WBy5Q90QjQ==
    签名生成 Golang 代码示例,仅供参考:
func GenSign(method, url, timestamp, nonce, body string, privateKey *rsa.PrivateKey) (string, error) { //组装被加密的字符串 targetStr := method + "\n" + url + "\n" + timestamp + "\n" + nonce + "\n" + body + "\n" //加密 h := sha256.New() h.Write([]byte(targetStr)) digestBytes := h.Sum(nil) signBytes, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, digestBytes) if err != nil { return "", err } sign := base64.StdEncoding.EncodeToString(signBytes) return sign, nil }
    签名生成 PHP 代码示例,仅供参考:
public function makeSign($method, $url, $body, $timestamp, $nonce_str) { $text = $method . "\n" . $url . "\n" . $timestamp . "\n" . $nonce_str . "\n" . $body . "\n"; $priKey = file_get_contents("/private_key.pem"); $privateKey = openssl_get_privatekey($priKey, ''); openssl_sign($text, $sign, $privateKey, OPENSSL_ALGO_SHA256); $sign = base64_encode($sign); return $sign; }
    签名生成 Java 示例,仅供参考:
public static String getSignature(String privateKeyStr, String method, String url, String body) throws Exception { Long timestamp = getTime(); String nonce = getNonce(); StringBuffer buffer = new StringBuffer(); buffer.append(method).append("\n"); buffer.append(url).append("\n"); buffer.append(timestamp).append("\n"); buffer.append(nonce).append("\n"); buffer.append(body).append("\n"); Signature sign = Signature.getInstance("SHA256withRSA"); sign.initSign(string2PrivateKey(privateKeyStr)); sign.update(buffer.toString().getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(sign.sign()); } // string2PrivateKey 仅供参考 public static PrivateKey string2PrivateKey(String privateKeyStr) { PrivateKey prvKey = null; try { byte[] privateBytes = Base64.getDecoder().decode(privateKeyStr); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateBytes); KeyFactory keyFactory = KeyFactory.getInstance(RSA); prvKey = keyFactory.generatePrivate(keySpec); } catch (Exception ex) { ex.printStackTrace(); } return prvKey; }

第三步:设置 HTTP Header

签名信息通过 HTTP 头Byte-Authorization传递,Byte-Authorization由认证类型和签名信息两部分组成。
Byte-Authorization: 认证类型 签名信息
    认证类型:
    目前为 SHA256-RSA2048
    签名信息:
    授权小程序应用 appid
    请求随机串 nonce_str
    请求时间戳 timestamp
    公钥版本 key_version
    签名值 signature
注意
    以上五项签名信息,无顺序要求。按照以下示例格式,key="value",签名信息之间用英文逗号,隔开。
    请求随机串和请求时间戳必须和计算签名值时使用的请求随机串和请求时间戳保持一致。
    公钥版本必须填写计算签名值时采用的应用私钥对应的应用公钥版本,应用公钥版本可通过服务商平台「开发-开发配置-密钥设置」处获取。
    代开发服务商代授权小程序发起调用时,签名信息中使用的是授权小程序应用的appid,非第三方应用id。
示例如下:
curl -v -d '{"appid":"ttxxx","order_id":"xxx"}' -H 'Byte-Authorization: SHA256-RSA2048 appid="ttxxx",nonce_str="DC10180A100073E70A48F195DA2AF2E6",timestamp="1623934869",key_version="1",signature="nwd1L3wCX+01/TVTkILeovF1DtYeghC1VHjrcjTHVkh7+gRaONEQkC2Y72Mw8JdSnIyeAtyp/pDHzyKGywjVqv5+JOBEhQG1/pvwNHN49wD26qg3AJL4hXw0fMJSRiTQEV1MszwDLuaabvo/qM9OXL9KyYiEPwVJqYtzmho4cHXT6mYgzNOW1xt5d7RDf4QO74JI3i4dtk9Uj8svJTrrBabML6AUcqcx2OP/7xukdaUgPdPf+IqmMG6GC4n52LUDogcL5n/osLdfHg9l6kW5gDcDjBfNDaggz07QMPHGdVao7pnQ2ub7VqcFIuY6Q3cBL7ndQdDGKrv+WBy5Q90QjQ=="' -H 'Content-Type: application/json' -H 'Accept: application/json' -X POST https://webcast.bytedance.com/api/business/diamond/query
请求示例如下:
curl -v -d '{"appid":"ttxxx","order_id":"xxx"}' -H 'Byte-Authorization: SHA256-RSA2048 appid="ttxxx",nonce_str="DC10180A100073E70A48F195DA2AF2E6",timestamp="1623934869",key_version="1",signature="nwd1L3wCX+01/TVTkILeovF1DtYeghC1VHjrcjTHVkh7+gRaONEQkC2Y72Mw8JdSnIyeAtyp/pDHzyKGywjVqv5+JOBEhQG1/pvwNHN49wD26qg3AJL4hXw0fMJSRiTQEV1MszwDLuaabvo/qM9OXL9KyYiEPwVJqYtzmho4cHXT6mYgzNOW1xt5d7RDf4QO74JI3i4dtk9Uj8svJTrrBabML6AUcqcx2OP/7xukdaUgPdPf+IqmMG6GC4n52LUDogcL5n/osLdfHg9l6kW5gDcDjBfNDaggz07QMPHGdVao7pnQ2ub7VqcFIuY6Q3cBL7ndQdDGKrv+WBy5Q90QjQ=="' -H 'Content-Type: application/json' -H 'Accept: application/json' -X POST https://webcast.bytedance.com/api/business/diamond/query

签名验证

代开发服务商应按照下述步骤验证成功应答(HTTP 状态码为 2xx)的签名信息。

第一步:构造验签名串

应答验签名串一共有 3 行,每行以\n结束,包括最后一行。\n为换行符(ASCII 编码值为 0x0A)。若应答报文主体为空(如:HTTP 状态码为 204 No Content),最后一行仅为一个\n换行符。
应答时间戳\n 应答随机串\n 应答报文主体\n
    应答时间戳
    从应答 HTTP 头Byte-Timestamp中获取应答时间戳。
    应答随机串
    从应答 HTTP 头Byte-Nonce-Str中获取应答随机串。
    应答报文主体
    应答中的应答 JSON 报文主体(response body)。

第二步:获取应答签名

应答签名值通过 HTTP 头Byte-Signature传递。如:
Byte-Signature: hWsiaADxS4OKLW/0JpDXiiji+GNRIsnnXsux3nVdyk7X6dqoyyJVYloQR9h/C1DIhGeBKe0i1iciyp6uq4LIkScyQKLhwEaXnWpcYat3+SAgS3ZYcGFY/op/MTO1bf172wbQBamC6gwydOWF0tWlMQb33ZYhztEDnD8iw/JkogOGHjO5uo869xWbgcq0OrkRN4zPGpOc/eiOR/B7fzxbasdMZtENOQMpgPP0z3k/cgeG/DSOwtwfA0eYnpYC8YqvKZ52HI5aCPkexmkfzUqNl1tbVylbMKDQDQoipQSxQPK2fxOFHj+jYu1TQ+nQFeu6amU/1rsMbT8JWa94bwwgkg==
对 Byte-Signature 的签名值使用Base64解码,得到应答签名。

第三步:校验签名

    使用平台公钥对验签名串和应答签名进行SHA256-RSA2048签名验证。 签名验证命令可参考:
# 1. 把Byte-Signature的签名值使用Base64解码,并保存到sign.txt文件中 $ openssl base64 -d -A <<< 'hWsiaADxS4OKLW/0JpDXiiji+GNRIsnnXsux3nVdyk7X6dqoyyJVYloQR9h/C1DIhGeBKe0i1iciyp6uq4LIkScyQKLhwEaXnWpcYat3+SAgS3ZYcGFY/op/MTO1bf172wbQBamC6gwydOWF0tWlMQb33ZYhztEDnD8iw/JkogOGHjO5uo869xWbgcq0OrkRN4zPGpOc/eiOR/B7fzxbasdMZtENOQMpgPP0z3k/cgeG/DSOwtwfA0eYnpYC8YqvKZ52HI5aCPkexmkfzUqNl1tbVylbMKDQDQoipQSxQPK2fxOFHj+jYu1TQ+nQFeu6amU/1rsMbT8JWa94bwwgkg==' > sign.txt # 2. 验证签名 $ openssl dgst -sha256 -verify public_key.pem -signature sign.txt << EOF 1623934990 49F0B152663446B14D57DDCA0D5418DB {"order_id":"xxx","order_status":2,"open_id":"openid","pay_tag":"参与游戏"} EOF Verified OK
    签名校验 Golang 代码示例,仅供参考:
func CheckSign(timestamp, nonce, body, signature, pubKeyStr string) (bool, error) { pubKey, err := PemToRSAPublicKey(pubKeyStr) if err != nil { return false, err } hashed := sha256.Sum256([]byte(timestamp + "\n" + nonce + "\n" + body + "\n")) signBytes, err := base64.StdEncoding.DecodeString(signature) if err != nil { return false, err } err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hashed[:], signBytes) return err == nil, nil } func PemToRSAPublicKey(pemKeyStr string) (*rsa.PublicKey, error) { block, _ := pem.Decode([]byte(pemKeyStr)) if block == nil || len(block.Bytes) == 0 { return nil, fmt.Errorf("empty block in pem string") } key, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return nil, err } switch key := key.(type) { case *rsa.PublicKey: return key, nil default: return nil, fmt.Errorf("not rsa public key") } }
    签名校验 PHP 代码示例,仅供参考:
public function verify($http_body, $timestamp, $nonce_str, $sign) { $data = $timestamp . "\n" . $nonce_str . "\n" . $http_body . "\n"; $publicKey = file_get_contents("/platform_public_key.pem"); if (!$publicKey) { return null; } $res = openssl_get_publickey($publicKey); $result = (bool)openssl_verify($data, base64_decode($sign), $res, OPENSSL_ALGO_SHA256); openssl_free_key($res); return $result; //bool }
    签名校验 Java 代码示例,仅供参考:
public static boolean verify(String httpBody, String publicKey, String signStr, Long timestamp, String nonce) throws Exception { StringBuffer buffer = new StringBuffer(); buffer.append(timestamp).append("\n"); buffer.append(nonce).append("\n"); buffer.append(httpBody).append("\n"); String message = buffer.toString(); Signature sign = Signature.getInstance("SHA256withRSA"); sign.initVerify(string2PublicKey(publicKey)); sign.update(message.getBytes(StandardCharsets.UTF_8)); return sign.verify(Base64.getDecoder().decode(signStr.getBytes(StandardCharsets.UTF_8))); } // string2PrivateKey 仅供参考 public static PublicKey string2PublicKey(String publicKey) throws Exception{ byte[] decoded = Base64.getDecoder().decode(publicKey); return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded)); }

FAQ

    生成公私钥
生成的公私钥可能会有 PKCS1 和 PKCS8 的区别。如果使用 JAVA,请将私钥转换成 PKCS8。
    签名生成的使用场景
签名生成是指代开发服务商在请求开放平台交易系统的 open api 时,需要携带签名等信息,代开发服务商使用自己生成的应用私钥给签名串生成签名,携带在 header 中。交易系统服务会使用代开发服务商上传的第三方小程序应用公钥进行签名验证,验证通过后放行请求。
    签名验证的使用场景
签名验证是开放平台交易系统使用平台私钥对签名串生成签名,代开发服务商使用平台公钥进行签名验证。签名验证有两个场景
    a.代开发服务商请求开放平台交易系统open api时,开放平台会放http resp的header中返回签名信息
    b.开放平台交易系统服务请求代开发服务商的api时(回调场景、状态通知等),交易系统服务会在http request的header中携带签名信息
    设置应用公钥时出错
如果报“参数错误”注意应用公钥应该是这样的格式,包含----BEGIN 一直到 END PUBLIC KEY----
    验签不通过
如果签名验证不通过,请代开发服务商先使用自己生成的公私钥测试加解密的过程,如果可以用私钥加密的数据,可以用公钥完成解密,再考虑以下几个因素
    1.确认当前第三方小程序已经在平台设置了公钥,设置方法参考上面的密钥设置环节
    2.确认上传的公钥与自己使用的公钥相同,以及和加签使用的私钥是匹配的
    3.检查服务商平台上是否更新了公钥。如果更新了公钥,确认签名使用的公私钥与版本key_version一致。key_version在公私钥设置页面可以看到。
    4.给body加解密的过程中,确保body数据严格一致,不能增加空格空行等。服务端以收到的http request body数据为准,开发者验证过程中也需要保证body和服务端收到的一致。
    5.拼接 "\n"的时候使用双引号,不要用单引号。
    6.确认生成签名的加签串和请求request中的数据一致。详细规则请看上面的加签环节。
    为什么有的接口验签可以通过,有的接口不能通过
开放平台的签名算法针对所有接口都是相同的逻辑,如果遇到这样的情况,有可能是中文字符串遇到转义问题。
验签时需要接收原始的 http request body 数据,不要进行任何处理。保证原始的 body 数据,作为字符串参与验签。如果是 JAVA 语言,回调接口需要用字符串接收,不要用 Bean 接收,Bean 接收参数会乱序导致验签不过。
    公私钥生成
公私钥生成应选择 2048bit。
上文中的 genrsa 命令生成的是 PKCS1 私钥和 PKCS8 公钥。需要上传的应用公钥也需要是 PKCS8 公钥。
不同语言有不同的处理,如果遇到验签不通过,请检查:
    如果使用java,请将生成的PKCS1私钥转换为PKCS8使用。
    如果使用Golang,直接使用生成的PKCS1私钥。如果生成的是PKCS8私钥,请转换成PKCS1使用。