基于自动应答的开发

指用户在Link手机端,访问已关注服务时,以聊天方式与服务号进行交互,处理过程如下:

开发步骤

  1. 以服务号创建者身份,进入服务号配置界面的“开发者选项”,配置服务号的“服务Url",并记录服务号的Token,之后用于验证平台请求的有效性

  2. 接收用户输入并处理 平台收到用户输入内容后,会以Post方式,将用户输入的内容提交到服务号的“服务Url"指定的地址上,为保证接口安全,第三方应用接收到推送过来的信息后,需要验证当前请求的有效性,即验证该请求是否来自Link服务器。平台每次调用接口时,会对接口参数进行签名,第三方通过签名验证平台的有效性。

    平台每次调用上面服务Url时,会传入以下信息:

    • signature:平台加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
    • timestamp:时间戳
    • nonce:随机数
    • serviceNoId:当前服务与Id
    • message:以Json格式存储的消息体,内包含用户输入内容,各字段说明见“Message字段说明”

      收到请求后,第三方可以按以下算法验证请求的有效性:

    • 将token、timestamp、nonce三个参数值进行字典序排序,并拼接成一个字符串,做为签名的key

    • 将请求中的其它参数值进行字典序排序,并拼接成一个字符串,做为data
    • 以HmacSHA1加密算法,使用key对data进行加密,获取加密串
    • 获得加密后的字符串可与signature对比,标识该请求来源于Link

Java类示例代码:

public boolean checkSignature(HttpServletRequest request) {
    // 服务号token
    String token = "XXXXXX";
    Map<String, String> bodyParamMap = new HashMap<String, String>();
    for (Object key : request.getParameterMap().keySet()) {
        String keyStr = key.toString();
        if (keyStr.equalsIgnoreCase(PortConsts.TIMESTAMP)
                || keyStr.equalsIgnoreCase(PortConsts.NONCE)
                || keyStr.equalsIgnoreCase(PortConsts.SIGNATURE)) {
            continue;
        }
        bodyParamMap.put(keyStr, request.getParameter(keyStr));
    }
    String signature = request.getParameter(PortConsts.SIGNATURE);
    String timestamp = request.getParameter(PortConsts.TIMESTAMP);
    String nonce = request.getParameter(PortConsts.NONCE);
    String sign = SignatureUtil.getSignature(token, timestamp, nonce,
            bodyParamMap.values().toArray(new String[] {}));
    if (sign.equals(signature)) {
        return true;
    }
    return false;
}

SignatureUtil代码:

/**
 * 签名辅助类
 * Created by ${yuananyun} on 2014/9/4.
 */
public class SignatureUtil
{
    private static final String HMAC_SHA1 = "HmacSHA1";
    public static final String TIMESTAMP = "timestamp";
    public static final String NONCE = "nonce";
    public static final String SIGNATURE = "signature";

    /**
     * 对Url进行签名
     * @param url 待签名的Url
     * @param token 签名用的token
     * @param postData url提交参数
     * @return url?timestamp=xx&nonce=xx&signature=xx
     */
    public static String signature(String url,String token,List<NameValuePair> postData){
        String timeStamp = String.valueOf(System.currentTimeMillis());
        String nonce = SignatureUtil.getRandomString(8);

        String[] paramArray = new String[postData.size()];
        int i = 0;
        for (NameValuePair pair : postData) {
            if(Strings.isEmpty(pair.getValue())){
                continue;
            }
            paramArray[i] = pair.getValue();
            i++;
        }

        String sign = getSignature(token, timeStamp, nonce, paramArray);
        String signedUrl = url;
        signedUrl = Urls.appendParam(signedUrl, TIMESTAMP, timeStamp);
        signedUrl = Urls.appendParam(signedUrl, NONCE, nonce);
        signedUrl = Urls.appendParam(signedUrl, SIGNATURE, sign);

        return signedUrl;        
    }



    /**
     * 对Url进行签名
     * @param url 待签名的Url
     * @param token 签名用的token
     * @param postData url提交参数
     * @return url?timestamp=xx&nonce=xx&signature=xx
     */
    public static String signature(String url,String token, MultiValueMap<String, Object> postData)
    {
        String timeStamp = String.valueOf(System.currentTimeMillis());
        String nonce = SignatureUtil.getRandomString(8);

        String[] paramArray = new String[postData.size()];
        int i = 0;
        for (List<Object> valueList : postData.values())
        {
            Object p = valueList.get(0);
            if (p == null)
            {
                continue;
            }
            paramArray[i] = p.toString();
            i++;
        }

        String sign = getSignature(token, timeStamp, nonce, paramArray);
        String signedUrl = url;
        signedUrl = Urls.appendParam(signedUrl, TIMESTAMP, timeStamp);
        signedUrl = Urls.appendParam(signedUrl, NONCE, nonce);
        signedUrl = Urls.appendParam(signedUrl, SIGNATURE, sign);

        return signedUrl;
    }


    /**
     * 生成摘要签名
     *
     * @param data 待加密的数据
     * @param key  加密使用的key
     * @throws InvalidKeyException
     * @throws NoSuchAlgorithmException
     */
    public static String getSignature(String data, String key) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException
    {
        byte[] keyBytes = key.getBytes("UTF-8");
        SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_SHA1);
        Mac mac = Mac.getInstance(HMAC_SHA1);
        mac.init(signingKey);
        byte[] rawHmac = mac.doFinal(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte b : rawHmac)
        {
            sb.append(byteToHexString(b));
        }
        return sb.toString();
    }

    /**
     * 生成通信签名
     *
     * @return
     */
    public static String getSignature(String token, String timeStamp, String nonce,String ... params)
    {
        /**
         * 生成签名
         */
        ArrayList<String> keyList = new ArrayList<String>();
        keyList.add(token);
        keyList.add(timeStamp);
        keyList.add(nonce);

        ArrayList<String> valueList = new ArrayList<String>();
        if(params!=null)
        for (Object param : params)
        {
            if(param==null) continue;
            valueList.add(param.toString());
        }
        return SignatureUtil.getMixSignature(valueList, keyList);
    }

    /**
     * 获取组合签名
     *
     * @param vList
     * @param kList
     * @return
     */
    public static String getMixSignature(List<String> vList, List<String> kList)
    {
        if(vList.size()>1)
        Collections.sort(vList);
        if(kList.size()>1)
        Collections.sort(kList);

        StringBuilder keyBuilder = new StringBuilder();
        for (String s : kList)
        {
            keyBuilder.append(s);
        }

        StringBuilder valueBuilder = new StringBuilder();
        for (String v : vList)
        {
            valueBuilder.append(v);
        }
        String key = keyBuilder.toString();
        String value = valueBuilder.toString();

        String sign;
        try
        {
            sign = SignatureUtil.getSignature(value, key);
        } catch (Exception e)
        {
            sign = null;
        }
        if (Strings.isBlank(sign))
        {
            throw new IllegalArgumentException("签名生成失败!");
        }
        return sign;
    }

    private static String byteToHexString(byte ib)
    {
        char[] Digit = {
                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
        };
        char[] ob = new char[2];
        ob[0] = Digit[(ib >>> 4) & 0X0f];
        ob[1] = Digit[ib & 0X0F];
        String s = new String(ob);
        return s;
    }


    /**
     * 获取指定才长度的随机字符串
     *
     * @param length
     * @return
     */
    public static String getRandomString(int length)
    {
        //length表示生成字符串的长度
        String base = "abcdefghijklmnopqrstuvwxyz0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++)
        {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }
}

目前已提供Java版本的SDK,实现了上述消息接收及平台验证的功能,见:服务号消息-》接收消息

Message内容

示例数据:

{
from_id:" ca2cbbbf-12c4-4bce-8abc-de60548d0223",
from_type:0,
to_id:" 2a50d696-cdaa-422d-b5f3-7de05e83e3fb",
to_type:"5",
content:{
    key: click_menu,
    value:code,
    params:’’
    }
}
  • from_id:与服务号交互的用户Id
  • from_type:0---系统,1---用户,2---群组,3---应用,4---部门,5---服务号,这里值一般为1
  • to_id:当前交互的服务号id
  • to_type: 0---系统,1---用户,2---群组,3---应用,4---部门,5---服务号,这里值一般为5
  • content:
    • 菜单点击事件消息格式
      • key=click_menu 表示用户点击菜单
      • value=菜单配置的code
      • params:此空
    • 自动应答(用户输入,或点击链接选择)事件消息格式
      • key=ivr_input 表示用户回复
      • value:上一条消息体中匹配到[reqeustAnswer]指令中key参数的内容,如果未匹配到,则直接为用户输入的内容,
      • params:上一条消息体中匹配到[reqeustAnswer]指令中params参数的内容,如果未匹配到[reqeustAnswer],则为空

接口调用返回值

第三方应用收到请求,处理完后,可选择两种方式返回结果给用户:

  • 委托平台返回结果给用户:该方式下,第三方需要按Link消息格式返回处理结果,Link服务器收到接口调用的返回结果后,转成消息数据,以当前服务号身份给用户发送处理结果,如返回文本消息(其它消息格式详见:《服务号消息》): { "msg_type": 1, "content": "GWNFGmZXG0VFGWJaGmpZ" }
  • 第三方系统直接通知用户:该方式下,第三方只需要返回一个空的处理结果回平台。之后,由第三方调用服务号发消息的功能,以该服务号身份给用户发送处理结果。
上一篇:菜单开发 下一篇:服务号消息