成 都 狮 龙 书 廊 科 技 有 限 责 任 公 司
Chengdu Shilong Pearson Education technology Limited Liability Technology Group Co., Ltd.
头条号网站验证文件
客服电话:13904310313
公司总机:028-67876373
钉钉客服:17684321066
备案电话:15680712313
商标注册服务电话:15210354365
公安部备案号:22010602000144
google-site-verification: googlea5d4809e7c237a00.html
SPI网关服务接入指南
¥100
对于ISV或者自研商家来说,只有官方同学添加相关场景的授权之后,开发者才可以在自己的控制台看到相关的场景及需要实现的接口及接口文档。
!
GET/POST
HTTPS
为了防止API调用过程中被黑客恶意篡改,抖店开放平台在发起调用任何一个API都会携带签名。ISV服务端在接收到请求后需要对请求做验签,确保请求是来自抖店开放平台的而不是其他的非法请求。目前从抖店开放平台发出的请求均采用MD5进行加密。
说明:验签只是为了验证参数有没有被篡改,所以服务商在接收到抖店开放平台的请求后**一定要把验签放在第一步、一定要把验签放在第一步、一定要把验签放在第一步。**验签测试阶段,我们构造的测试参数并不一定完全符合业务逻辑,但是这不会影响验签。一定要拿着我们原始的请求去做签名计算,而不是先反序列化或者解析成你们自己定义的结构体后用你们自己的结构体去做签名计算,这样有可能导致签名失败。即便现在不失败,后面我们新增参数的话,你们的接口可能会出兼容问题。
1、将param_json中参数按照key字母先后顺序排序,组成json
说明:如果是GET请求 param_json请从url中获取,如果是post请求,param_json的value为post请求的body
2、请求参数按照字母先后顺序排列
3、把参数名和参数值进行拼装
4、把app_secret分别拼接在上面得到的字符串的两端,假定app_secret的值为AppSecret
5、上述步骤获得的待加密字符串,使用 MD5签名算法后得到sign,与请求中的sign进行比较,如果不一致则验签失败。
注:计算签名逻辑请使用下面的示例代码!!!不要自己实现
SPISDK已经上线,推荐使用SDK对接,SDK说明文档:抖店开放平台SPI&消息网关SDK,使用SDK对接可跳过下面示例代码部分
以接口“demo/spi”为例
public Object spiDemo() { DemoSpiRequest request =new DemoSpiRequest(); //param中的参数从服务端调用你们发布的地址上取 //sign timestamp sign_method app_key sign_v2从URL参数取 //paramJson分为两种情况,如果是get请求,从URL参数取,如果post请求从请求体里取 //举个例子,发布的地址是http://www.abc.com //服务端实际调用时的请求地址是:http://www.abc.com?app_key=xxx&sign=xxx&sign_v2=xxx&sign_method=xxx×tamp=xxx¶m_json=xxx request.getParam().setSign( xxxx ); request.getParam().setTimestamp( xxxx ); request.getParam().setSignMethod( xxxx ); request.getParam().setAppKey( xxxx ); request.getParam().setParamJson( xxxx ); request.getParam().setSignV2( xxxx ); request.registerHandler(bizHandler); DemoSpiResponse response = request.execute(); //将response序列化成json返回 return response; } private DoudianOpSpiBizHandler bizHandler = new DoudianOpSpiBizHandler() { @Override public void handle(DoudianOpSpiContext context) { // 获取入参对象 DemoSpiParam param = context.getParam(); // 业务处理逻辑 // ... // 设置data数据 DemoSpiData data = context.getData(); // data.setXXX() // 返回成功 context.wrapSuccess(); // 返回失败 context.wrapError(100002L,系统错误 ); } };
import ( "code.byted.org/middleware/hertz" "crypto/md5" "encoding/hex" "encoding/json" "io" "sort" ) // TestSpi . // @router /api/spi/test [POST] func TestSpi(c *hertz.RequestContext) { //1.从请求中解析入参 appKey := c.Query("app_key") paramJson := c.Query("param_json") timestamp := c.Query("timestamp") sign := c.Query("sign") //2.参数校验,此处省略 //3.验签 //3.1.解析业务参数 paramJsonInterfaceMap := make(map[string]interface{}) err := json.Unmarshal([]byte(paramJson), ¶mJsonInterfaceMap) if err != nil { c.JSON(200, &DemoResult{ Code:100002, Message: "参数解析失败", }) return } //3.2.把param_json的key按照自然顺序排序 mm := getJsonString(paramJsonInterfaceMap) sortedParamJson, _ := json.Marshal(mm) sortedString := "app_key" + appKey + "param_json" + string(sortedParamJson) + "timestamp" + timestamp //3.3.拼接 appSecret 在头尾 appSecret := "63415a7a-de83-43ea-a522-cb616c47a4ef" preSignString := appSecret + sortedString + appSecret //3.4使用md5对参数加签 newSign := Md5(preSignString) //3.5.将入参中的sign和加签后的结果作比较,如果不一致则验签失败。 if newSign != sign { c.JSON(200, &DemoResult{ Code:100001, Message: "验签失败", }) return } //4.业务处理,此处省略 //5.组装响应结果 c.JSON(200, &DemoResult{ Code:0, Message: "success", Data: &TestResult{ Total: 100, Test:false, List: []*RefundOrder{ &RefundOrder{ RefundId: "11111", RefundReason: "七天无理由", }, }, }, }) } func Md5(s string) string { h := md5.New() _, _ = io.WriteString(h, s) return hex.EncodeToString(h.Sum(nil)) } func getJsonString(param map[string]interface{}) interface{} { sortedKeys := make([]string, 0) for k := range param { sortedKeys = append(sortedKeys, k) } sort.Strings(sortedKeys) sortedParamJsonMap := make(map[string]interface{}) for _, sk := range sortedKeys { switch value := param[sk].(type) { case string: sortedParamJsonMap[sk] = value case map[string]interface{}: sortedParamJsonMap[sk] = getJsonString(value) case []interface{}: if len(value) > 0 { if _, ok := value[0].(map[string]interface{}); ok { var sliceResult = make([]interface{}, 0) for _, interfaceValue := range value { switch mapInterfaceValue := interfaceValue.(type) { case map[string]interface{}: sliceResult = append(sliceResult, getJsonString(mapInterfaceValue)) } } sortedParamJsonMap[sk] = sliceResult } else { sortedParamJsonMap[sk] = value } } else { sortedParamJsonMap[sk] = make([]interface{}, 0) } default: sortedParamJsonMap[sk] = value } } return sortedParamJsonMap } type DemoResult struct { Codeint64 `json:"code"` Message string`json:"message"` Data*TestResult `json:"data"` } type TestResult struct { Total int64`json:"total"` Testbool `json:"test"` List[]*RefundOrder `json:"list"` } type RefundOrder struct { RefundId string `json:"refund_id"` RefundReason string `json:"refund_reason"` } import ( "code.byted.org/middleware/hertz" "crypto/md5" "encoding/hex" "encoding/json" "io" "sort" ) // TestSpi . // @router /api/spi/test [POST] func TestSpi(c *hertz.RequestContext) { //1.从请求中解析入参 appKey := c.Query("app_key") paramJson := c.Query("param_json") timestamp := c.Query("timestamp") sign := c.Query("sign") //2.参数校验,此处省略 //3.验签 //3.1.解析业务参数 paramJsonInterfaceMap := make(map[string]interface{}) err := json.Unmarshal([]byte(paramJson), ¶mJsonInterfaceMap) if err != nil { c.JSON(200, &DemoResult{ Code:100002, Message: "参数解析失败", }) return } //3.2.把param_json的key按照自然顺序排序 mm := getJsonString(paramJsonInterfaceMap) sortedParamJson, _ := json.Marshal(mm) sortedString := "app_key" + appKey + "param_json" + string(sortedParamJson) + "timestamp" + timestamp //3.3.拼接 appSecret 在头尾 appSecret := "63415a7a-de83-43ea-a522-cb616c47a4ef" preSignString := appSecret + sortedString + appSecret //3.4使用md5对参数加签 newSign := Md5(preSignString) //3.5.将入参中的sign和加签后的结果作比较,如果不一致则验签失败。 if newSign != sign { c.JSON(200, &DemoResult{ Code:100001, Message: "验签失败", }) return } //4.业务处理,此处省略 //5.组装响应结果 c.JSON(200, &DemoResult{ Code:0, Message: "success", Data: &TestResult{ Total: 100, Test:false, List: []*RefundOrder{ &RefundOrder{ RefundId: "11111", RefundReason: "七天无理由", }, }, }, }) } func Md5(s string) string { h := md5.New() _, _ = io.WriteString(h, s) return hex.EncodeToString(h.Sum(nil)) } func getJsonString(param map[string]interface{}) interface{} { sortedKeys := make([]string, 0) for k := range param { sortedKeys = append(sortedKeys, k) } sort.Strings(sortedKeys) sortedParamJsonMap := make(map[string]interface{}) for _, sk := range sortedKeys { switch value := param[sk].(type) { case string: sortedParamJsonMap[sk] = value case map[string]interface{}: sortedParamJsonMap[sk] = getJsonString(value) case []interface{}: if len(value) > 0 { if _, ok := value[0].(map[string]interface{}); ok { var sliceResult = make([]interface{}, 0) for _, interfaceValue := range value { switch mapInterfaceValue := interfaceValue.(type) { case map[string]interface{}: sliceResult = append(sliceResult, getJsonString(mapInterfaceValue)) } } sortedParamJsonMap[sk] = sliceResult } else { sortedParamJsonMap[sk] = value } } else { sortedParamJsonMap[sk] = make([]interface{}, 0) } default: sortedParamJsonMap[sk] = value } } return sortedParamJsonMap } type DemoResult struct { Codeint64 `json:"code"` Message string`json:"message"` Data*TestResult `json:"data"` } type TestResult struct { Total int64`json:"total"` Testbool `json:"test"` List[]*RefundOrder `json:"list"` } type RefundOrder struct { RefundId string `json:"refund_id"` RefundReason string `json:"refund_reason"` }
127.0.0.1:6789/shop/user/register?app_key=6900812651828348424¶m_json=%7B%22order_id%22%3A%221234%22%2C%22page%22%3A10%2C%22size%22%3A11%7D&sign=6c4447b0bf1898d38f78ab80f7d86e46×tamp=2021-06-01+21%3A49%3A17
{ "code": 0, "message": "success", "data": { "total": 100, "test": false, "list": [ { "refund_id": "11111", "refund_reason": "七天无理由" } ]
{ "code": 100001, "message": "验签失败", "data": null }
错误码 | 说明 |
---|---|
0 | 业务处理成功 |
100001 | 验签失败 |
100002 | 参数错误 |
100003 | 系统错误 |
更多业务错误码请参考各个SPI接口的接口文档说明。
ISV在完成代码研发后,可以在抖店开发者控制台发起接口自测,自测通过之后可以将服务端url去发布。
接口自测通过后可以将服务端地址发布到对应的接口上。接口发布成功之后字节内部的业务就可以发起相关的调用啦。
为了方便排查问题,抖店开放平台在向服务商的地址发起请求时会把平台的logId放到请求的hedaer中,建议开发者在接受到请求后,除了打印请求入参外把logId也打印或者记录下来。