签名算法
收藏
我的收藏遇到任何签名问题强烈建议请先通读本文,从历史经验看,平台当前遇到的 oncall 问题都可以从本文中找到答案,提 oncall 问题并得到解答的时间往往比通读本文自行解决要慢,因此强烈建议遇到签名问题时先从本文中寻找答案。
签名算法
鉴权认证机制采用
SHA256-RSA2048
实现,字符编码采用 UTF-8 编码集。本签名规范中,加签和验签的原文组装方式略有不同,请严格按照规范要求进行组装。
使用说明
在鉴权认证机制中,需要 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
上传方式
平台公钥
平台公钥和私钥由「开放平台」负责生成和保存,不同「小程序」的平台公钥和私钥是不同的。
获取方式
开发指南
生成签名
「小程序」应按照下述步骤生成请求的签名信息。
第一步:构造待签名串
待签名串一共有五行,每一行必须以
\n
(换行符,ASCII 编码值为 0x0A)结束。HTTP请求方法\nURI\n请求时间戳\n请求随机串\n请求报文主体\n
其中:
- •HTTP 请求方法
如:POST,GET,PUT 等。注意请使用大写。
- •URI
获取请求的开放平台接口的绝对 URL,并去除域名部分得到参与签名的 URI。 URI 必须以斜杠字符“/”开头。 如
PATH=https://open.douyin.com/api/trade/v2/query URI 则为 /api/trade/v2/query PATH=https://open.douyin.com/api/trade/v2/query?a=x URI 则为 /api/trade/v2/query?a=x
- •请求时间戳
发起请求时系统的当前时间戳,即格林威治时间 1970 年 01 月 01 日 00 时 00 分 00 秒(北京时间 1970 年 01 月 01 日 08 时 00 分 00 秒)起至现在的总秒数,作为请求时间戳。「开放平台」会拒绝处理一个小时之前发起的请求。
- •请求随机串
任意生成一个随机字符串(长度不限制),以保证相同时间相同参数发起的请求签名值不一样。
- •请求报文主体
获取请求中的请求报文主体(request body)。
- ◦请求方法为GET时,报文主体为空。
- ◦当请求方法为POST或PUT时,请使用报文内容。
第二步:计算签名值
「小程序」成功构造待签名串后,「小程序」使用应用私钥对待签名串进行
SHA256-RSA2048
签名,并对签名结果进行Base64
编码得到签名值。第三步:设置 Header
签名信息通过 HTTP 头
Byte-Authorization
传递,Byte-Authorization
由认证类型和签名信息两部分组成。Byte-Authorization: 认证类型 签名信息
- •认证类型:
目前为
SHA256-RSA2048
- •签名信息:
- ◦应用
appid
- ◦请求随机串
nonce_str
- ◦请求时间戳
timestamp
- ◦公钥版本
key_version
- ◦签名值
signature
注意:
- •以上五项签名信息,无顺序要求。按照以下示例格式,key="value",签名信息之间用英文逗号,隔开。
- •请求随机串和请求时间戳必须和计算签名值时使用的请求随机串和请求时间戳保持一致。
- •公钥版本必须填写计算签名值时采用的应用私钥对应的应用公钥版本,应用公钥版本可通过「开发管理-开发设置-密钥设置」处获取。
示例如下:
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)的签名信息。
第一步:构造待签名串
应答验签名串一共有三行,每行必须以
\n
(ASCII 编码值为 0x0A)结束。应答时间戳\n应答随机串\n应答报文主体\n
- •应答时间戳
从应答 HTTP 头
Byte-Timestamp
中获取应答时间戳。- •应答随机串
从应答 HTTP 头
Byte-Nonce-Str
中获取应答随机串。- •应答报文主体
应答中的报文主体(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
签名验证。示例代码
签名示例代码
JAVA
GO
PHP
public static String getSignature(String privateKeyStr, String method, String uri, long timestamp, String nonce, String body) throws Exception { //method内容必须大写,如GET、POST,uri不包含域名,必须以'/'开头 String rawStr = method + "\n" + uri + "\n" + timestamp + "\n" + nonce + "\n" + body + "\n"; Signature sign = Signature.getInstance("SHA256withRSA"); sign.initSign(string2PrivateKey(privateKeyStr)); sign.update(rawStr.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(sign.sign()); } 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; }
验签示例代码
JAVA
GO
PHP
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)); // 注意验签时publicKey使用平台公钥而非应用公钥 sign.update(message.getBytes(StandardCharsets.UTF_8)); return sign.verify(Base64.getDecoder().decode(signStr.getBytes(StandardCharsets.UTF_8))); } public static PublicKey string2PublicKey(String publicKey) throws Exception{ byte[] decoded = Base64.getDecoder().decode(publicKey); return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded)); }
自查数据
以下提供了一套标准 PKCS8 格式的 2048 位密钥和待签名串和签名串,开发者可以自行验证签名验签代码是否正确。
-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCZSHNcFfthd/bV YexEJWOBVEjjDcXjfr1fYevuraNFfMmLPKV836BbvCiUSWHzJYEpkJ934e/j28NB EcEbPDLiGlLTd6AVwR22TkUwpLr41oQprz0HKFwhVPZ0HQCGIv0pVMA53TFSitIq iqbNLmgm5yzSNqNy1t/0X/RfqEtA6Eoxw9u/Sx57i+pBFuLlZYanlm57+b7t1khg 9JHvF0ulo7DScyJ4qgrD7oQf0RIQB0rqCFIeYuYO1cfvnxb9x4DPodEyVoAM4i9Y dFop9ZHt73W/icuLku/P8/G1+arzB5b7S1S3ky5/KdS8AEA9Ww5czZcdf9Jgm2S6 RymjFGjzAgMBAAECggEBAIryGNgdePyWcSJmHHR9a+CdFWD0aDBa/7CJpAN8VKc1 gcB8Xgp+7+6X9jTM/EQa+CVEWrmiDgF/gVPnkyNsAzff4rqcEnoFzzglZSS9/lp4 od7jYa+uTy1LxgflDkeJSfEASStqrT4EZpR3kNInQfQZ1BBNxQXhb6smm+9mL6kK QJjAqBgEqtUAmNv0GnH89ZPPgZuIZeeL4cb4BhMEoa5MBnI+HDf07cN1nECQXRJl HU/iyhAPfP7RpO8O9KGDEDE36qebu0Cu4yUjWANXiqECFv93sQzONotkl3VPealv XM+jzGT7YdgHo/t3QKE8flMBo/XUzGTqi8j5AOXiaBkCgYEAylKVtjQMYgg4qMwd 9Je+KZ9qL6QVHCsB2NPUt8N99oj70efsG4aGaEAadr8meNhIJ5lpoK+FXqSBIbbD S/xeOVI3XoMx/EdKLw/ZNi87G/EHYK9z7Fr3W7q8DFXe2hZ/ojFXC/aaBanjVVBK /6RfPzXfnx+vGX/t1FhcLC+yQD8CgYEAwfMtrXfH+3dW77dxXT/CTFJVs/o1K2qb epnQ6A33KMHPLBtPZZ6z5rzIO7OMSNItOTXTEoRXHmOKc5FtXGtbCvGSRByb6FgD WG3p2Bp2sZLuJBzXmLbSnEbHTNHM6uTgxNgWAh8pYpjPY8xF7BqYz2rGT47OPBmc tRzDjnzjak0CgYAqkM1mk/S2+zvQZ4E14GblouBYPZEjZ/jvgUGTl9F8eL1iIAUQ lXDZpgLrULPrYLVtf101rTfF/Z4dVbIo3mOEc8OqYre1d9onpJHyUGWDL2Z59O/S niDEb7j4b2h/QZSArxi9L5if8GofnNDqj85qIg92Dthr6PpEXoKl2TMLSQKBgFzQ BHHYukiqSV4ZyRQ4qMBhPkYMXFlUgObgqMoDtN06MewHfa1BjxHCEYgQWfeXLLEO At3/mrkeJWk8lLr/XOgVxkr17d34EFHG93rE3zwG9hMuAjZAdvT2IfWvCIL32GAa kB2fz+ww+D3nySY9bBcGH7R+wE6eaxF4nFSZizKZAoGBAJzuaWCnVK0djfgvUsjm GUtyDvgyREcpAsXvES1pB2NLVeEUxm0uRtj6k4DhCv3rJfUwfMr0+sa9NUnXuaSR VqLYvAD8bNPKXwn7ymzQ7WioCqmZuUhLnQRppkjhfQGKLH0MnMw9Xh9FwJ9kzGNE UnTEhaaHsoaHMlLlRET32gyG -----END PRIVATE KEY----- -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmUhzXBX7YXf21WHsRCVj gVRI4w3F4369X2Hr7q2jRXzJizylfN+gW7wolElh8yWBKZCfd+Hv49vDQRHBGzwy 4hpS03egFcEdtk5FMKS6+NaEKa89ByhcIVT2dB0AhiL9KVTAOd0xUorSKoqmzS5o Jucs0jajctbf9F/0X6hLQOhKMcPbv0see4vqQRbi5WWGp5Zue/m+7dZIYPSR7xdL paOw0nMieKoKw+6EH9ESEAdK6ghSHmLmDtXH758W/ceAz6HRMlaADOIvWHRaKfWR 7e91v4nLi5Lvz/Pxtfmq8weW+0tUt5MufynUvABAPVsOXM2XHX/SYJtkukcpoxRo 8wIDAQAB -----END PUBLIC KEY----- 注意:待签名串只作为示例使用,与签名验签规范无关。 待签名串:"POST\n/abc\n1680835692\ngjjRNfQlzoDIJtVDOfUe\n{\"eventTime\":1677653869000,\"status\":102}\n" 签名串:"RFQ65hHlo4xyZ6EC31LZC0SzsyN0nd2Fb2wAiISvY1mkiC6G8gn2QZwLGq7qgjenRGl/Z8OrTtkBHWb9GOazkJFkHrPeRqogqnwZ+kSOxGvtou8FPN669E1wwb+BShN4pIUgPFzaukR9/rCRBsbLoq9RPVA2sbf3iKoHGa81zhXjQSuFbF1CyiWkL5qqniNTM/BSfwfLZfPW8nBanRl3U+mQaymbj0DCF0ZdWhFz1FnZPAfEpx8YEwFNZWtxzz4p3WJ1swnUocJC4LXoDazo6DhEPDuoZXOXrB1SqzL1wRqA4p8uj3Z8Seki/PMGWiGpGWPMv3tJyvWmzMOuVJtEjg=="
注意事项
- 1.密钥长度必须为2048位
- 2.不同的签名验签代码,对密钥格式有不同的要求,比如JAVA一般使用PKCS8格式的密钥,其他语言一般使用PKCS1格式的密钥(区分小技巧:开头为"-----BEGIN PRIVATE KEY-----"的密钥格式为PKCS8,开头为"-----BEGIN RSA PRIVATE KEY-----"的密钥格式为PKCS1)
- 3.每次提交新的应用公钥,公钥版本会发生变化
- 4.请注意使用原生的request body中的内容进行验签,避免因框架解析导致字段顺序变化。
- 5.如果使用的低版本的JDK(JDK7及以下),请不要使用sun.misc.BASE64包进行base64编解码,请使用org.apache.commons.codec.binary.Base64包或者javax.xml.bind.DatatypeConverter包
- 6.java中使用gjson做序列化的时候可能出现long类型参数被实际序列化成了string类型,而请求发送给平台的时候对应参数仍然是long类型从而导致平台验证签名无法通过,这种情况下请使用(new GsonBuilder()).setLongSerializationPolicy(LongSerializationPolicy.DEFAULT).create()创建gjson对象,同时通过调整LongSerializationPolicy的值来根据实际需求情况来对long类型的值进行序列化
FAQ
加签问题
用于排查开发者请求交易系统api时,平台签名校验异常的问题
- 1.若接入通用交易系统,请优先排查以下几步——
- a.请优先使用平台提供的示例代码,可参考此处。
- b.前端代码是否对服务端下发的data或byteAuthorization做了转化。
- c.服务端参与加签的data是否原样返回给了前端,若中间经过修改,则一定会签名校验异常。
- 2.(重要) 请开发者在服务端接口的出口位置,及服务端加签方法的出入口位置等,都进行日志打印,这将在很大程度上提升定位问题的效率。
- 3.确认当前参与签名的小程序已经在平台设置了公钥,设置方法参考上面的密钥设置环节。
- 4.确认签名使用的公钥版本key_version,是否和开发者后台显示的key_version一致。
- 5.借助签名调试工具,自查应用公钥和应用私钥是否匹配,并自查是否和工具中「签名-生成签名」模块生成的结果一致。
- 6.确认密钥格式是否正确,JAVA适用PKCS8(关键字为BEGIN PRIVATE KEY),非JAVA适用PKCS1(关键字为BEGIN RSA PRIVATE KEY)
- 7.密钥长度是否为2048位,若不确定,可直接使用签名调试工具生成密钥。
验签问题
用于排查交易系统服务请求开发者的api时,开发者验签异常的问题
- 1.若接入通用交易系统,请优先使用平台提供的示例代码,可参考此处。
- 2.(重要) 请开发者在接口入口处及其它关键位置打印日志,这在很大程度上能提升定位问题的效率。
- 3.(重要) 验签时需要接收原始的 http request body 数据,并不要进行任何处理。保证原始的 body 数据,作为字符串参与验签。如果是 JAVA 语言,
- a.回调接口需要用字符串接收,不要用 Bean 接收,Bean 接收参数会乱序导致验签不过。
- b.在读取网络包的时候如果使用了readLine函数则可能导致验签通不过,因为readLine默认会在每次读取的时候在行位append '\n'字符。
- 4.拼接 "\n"的时候使用双引号 不要用单引号。
Oncall 指引
为减少 oncall 沟通过程提高问题解决效率,请提 oncall 的时候务必提供以下内容:
- •签名问题:请提供参与签名的全部参数,提供签名计算代码。
- •验签问题:请提供接收到的平台请求包原始内容,提供验签代码。