无线开放 API

简述

为了保护某些敏感的数据的安全性,我们提供了一套全新的接口请求方式:SPI。目前已开放的接口有购物车回调、手淘专用红包发放等,后续会有更多的接口会通过这种方式开放。

术语约定

  • SPI :一种淘宝和三方服务器间通信机制,常见的使用方式,由淘宝定义服务接口,三方提供服务实现(一个可访问的回调地址)。
  • 场景 :一组spi接口的集合,spi的权限管理单位。
  • 回调地址:SPI服务的实现。一个可访问的http链接,请求格式中的query和body参数由“SPI接口文档”的入参说明,响应的格式需要符合“SPI接口文档”的出参说明。

SPI接口开发基础流程

Step 1、获取SPI场景使用入口权限

把互动应用后台appkey提供给SPI的发布人( 书通 )。

注:“购物车回调”,“颁发onecode”两个场景已经默认开放,无需此步骤

Step 2、到TOP后台确认是否开通

(例如:http://my.open.taobao.com/spi/groups.htm?appkey=23025543&app_id=1744060

Step 3、申请场景开发

申请之后,需要主动通知一下SPI提供者( 书通 )进行审批。

Step 4、找到SPI场景的开发入口

在TOP控制台“我的场景”,点击进入开发

Step 5、配置回调地址

点击“开发测试”,进入页面后配置自己的后台页面地址,该页面地址用来接收 数据请求

Step 6、SPI服务开发(含源代码)

开始页面开发,比如测试页面链接

http://jiuxianphone-1.play.admin.jaeapp.com/spiCart.jsp

页面格式输出格式(json或者xml)以及页面输出参数需要参照场景开发文档。

页面JSP示例代码如下:

<%@ page contentType="application/json; charset=UTF-8" %>
<%
response.setContentType("text/xml");
%>
<recieved>true</recieved>

源代码SVN地址下载

账号:b2ctest17@yahoo.cn 密码:sxc50113891

页面PHP示例代码如下:待补充。

Step 7、确认是否能收到消息

坐等接收请求,程序需要有servlet来处理该Http get请求,TOP会发送一条以下格式的Http get请求(以购物车接口为例):

http://jiuxianphone-1.play.admin.jaeapp.com/spiCart.jsp?sign=D645ACB8A1E8FEB32E0AEF4965B0C5FC&timestamp=2015-04-10+17%3A57%3A17&sellerNick=%E5%95%86%E5%AE%B6%E6%B5%8B%E8%AF%95%E8%B4%A6%E5%8F%B7&skuId=12123&itemId=12312321&mixBuyerNick=1321231321

Step 8、校验请求真实性(验签)

验签代码SignUtil中的secret字段需要根据应用配置填写。

  • Java方式:
package tae.util;

import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.*;

public class SignUtil {

    private static final Logger logger = Logger.getRootLogger();
    private static final String HEADER_PARAM_PREFIX = "header_";
    private static final String TOP_SIGN_LIST = "top_sign_list";

    private static final String SECRET = /*根据应用secret配置信息填写*/; 

    public static ThreadLocal<String> body = new ThreadLocal<String>();

    /**
     * @param headerMap http请求中header中的参数,包括”top_sign_list“域,这个域中用”,"连接了所有需要参与sign的header参数;
     *                  如果某参数没有值,也需要放入headerMap中,相应的values为""
     * @param queryMap  http请求中query中的参数
     * @param body      从http请求中取出的输入流(可参考getBody方法)
     * @param secret    签名密钥
     * @param encode    服务的编码方式(在配置服务url的页面中可以看到)
     * @return
     * @throws Exception
     */
    public static String sign(Map<String, String> headerMap, Map<String, String> queryMap, String body, String secret, String encode) throws Exception {

        //取出header中的top_sign_list,它的内容是用","隔开的参数名,这些参数需要参与sign
        //这么做的原因是header有可能会被改写
        String topSignList = headerMap.get(TOP_SIGN_LIST);
        if (StringUtils.isNotBlank(topSignList)) {
            String[] headerSignParams = StringUtils.split(topSignList, ",");

            for (int i = 0; i < headerSignParams.length; i++) {
                String headerSignParam = headerSignParams[i];

                String value = headerMap.get(headerSignParam);
                //为了避免与query中的参数同名,加上前辍
                queryMap.put(HEADER_PARAM_PREFIX + headerSignParam, value);
            }

        }

        String sign = signatureForSpi(queryMap, secret, true, false, body, encode);
        return sign;
    }

    public static String getBody(HttpServletRequest request) {
        InputStream inputStream = null;
        try {
            inputStream = request.getInputStream();
            String dataOrigin = IOUtils.toString(inputStream);

            return dataOrigin;
        } catch (Exception e) {
            logger.info("exception IOUtils.toString");
            throw new RuntimeException(e);
        }
    }

    private static String signatureForSpi(Map<String, String> params, String secret,
                                          boolean appendSecret, boolean isHMac, String body, String encode) throws Exception {
        StringBuilder sb = new StringBuilder();
        // append if not hmac
        if (!isHMac) {
            sb.append(secret);
        }
        if (params != null && !params.isEmpty()) {
            String[] names = params.keySet().toArray(ArrayUtils.EMPTY_STRING_ARRAY);
            Arrays.sort(names);
            for (int i = 0; i < names.length; i++) {
                String name = names[i];
                sb.append(name);
                sb.append(params.get(name));
            }
        }
        if (StringUtils.isNotBlank(body)) {
            sb.append(body);
        }
        if (appendSecret && !isHMac) {
            sb.append(secret);
        }
        String sign = null;
        try {
            //md5
            sign = DigestUtils.md5Hex(sb.toString().getBytes(encode)).toUpperCase();

        } catch (Exception e) {
            logger.info("exception DigestUtils.md5Hex");
            throw new RuntimeException(e);
        }
        return sign;
    }

    private static String date2ymdhms(Date date) {
        SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return f.format(date);
    }

    public static Boolean testSign(HttpServletRequest request, Map<String, Boolean> decodeConfig) {
        Map<String, String> headerMap = getHeadersInfo(request);

        //获得 UrlDecode后的header和body参数
        Map<String, String> headers = new HashMap<String, String>();
        String body = null;
        try {
            if (headerMap != null && !headerMap.isEmpty()) {
                String[] headerParams = headerMap.keySet().toArray(ArrayUtils.EMPTY_STRING_ARRAY);

                for (int i = 0; i < headerParams.length; i++) {
                    String name = headerParams[i];
                    headers.put(name, URLDecoder.decode(headerMap.get(name), "UTF-8"));
                }
            }

            body = URLDecoder.decode(getBody(request), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            logger.info(JSONObject.toJSONString(e.getStackTrace()));
        }

        //获得query参数,使用request.getParameterMap获得的参数有乱码
        Map<String, String> params = new HashMap<String, String>();
        String queryString = request.getQueryString();
        String[] param = StringUtils.split(queryString, "&");
        for (int i = 0; null != param && i < param.length; i++) {
            String[] kv = StringUtils.split(param[i], "=");
            if (kv.length == 2) {
                params.put(kv[0], kv[1]);
            }
        }
        Map<String, String> queryMap = new HashMap<String, String>();
        String sign = "";

        sign = params.get("sign");
        logger.info("sign : " + sign);

        copyMap(params, queryMap);

        logger.info("queryMap : " + JSONObject.toJSONString(queryMap));

        //签名
        String sign2 = "";
        try {
            sign2 = sign(headers, queryMap, body, SECRET, "UTF-8");
        } catch (Exception e) {
            logger.info(JSONObject.toJSONString(e.getStackTrace()));
        }
        logger.info("sign2 : " + sign2);

        return sign2.equals(sign);

    }

    private static void copyMap(Map<String, String> params, Map<String, String> queryMap) {
        for (String p : params.keySet()) {
            if ("sign".equals(p)) {
                continue;
            }
            String pValue = params.get(p);
            try {
                pValue = URLDecoder.decode(pValue, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                logger.info(JSONObject.toJSONString(e.getStackTrace()));
            }
            queryMap.put(p, pValue);
        }
    }

    private static Map<String, String> getHeadersInfo(HttpServletRequest request) {

        Map<String, String> map = new HashMap<String, String>();

        Enumeration headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = (String) headerNames.nextElement();
            String value = request.getHeader(key);
            map.put(key, value);
        }

        return map;
    }
}
  • PHP方式: 待补充

Step 9、提交上线申请