32

微信公众号接入之排序问题小记

 5 years ago
source link: http://www.cnblogs.com/yougewe/p/10012898.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

发微信公众号作为强大的自媒体工具,对接一下是很正常的了。不过这不是本文的方向,本文的方向公众号接入的排序问题。

最近接了一个重构的小项目,需要将原有的php的公众号后台系统,转换为java系统。当然,也很简单的了。

不过,在接入的时候,遇到有一个有趣的问题,可以分享下。

大家知道,要将微信在接到用户的请求之后,可以将消息转发给咱们在公众号后台指定的 server 地址,而在指定这个地址的时候,又需要先校验下这个地址是否连通的,是不是开发者自己的用来处理微信转发消息的地址。因此有一个服务器 token 的校验过程。

校验token的过程,算法很简单,引用微信文档原文如下:

开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:

1)将token、timestamp、nonce三个参数进行字典序排序

2)将三个参数字符串拼接成一个字符串进行sha1加密

3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信

php 示例如下:

private function checkSignature()
{
    _GET["signature"];
    _GET["timestamp"];
    _GET["nonce"];

    tmpArr = array(timestamp, $nonce);
    sort($tmpArr);        // 官方最新版demo已经修复该排序问题了 sort($tmpArr, SORT_STRING);
    $tmpStr = implode( $tmpArr );
    $tmpStr = sha1( $tmpStr );

    if( signature ){
        return true;
    }else{
        return false;
    }
}

而且,下方demo也给的妥妥的。好吧,对接是不会有问题了!

我也按照java版的demo,给整了个接入进来!java 的验证样例如下:

    public String validate(@ModelAttribute WxValidateBean validateBean) {
        String myValidToken = signatureWxReq(validateBean.getToken(), validateBean.getTimestamp(), validateBean.getNonce());
        if(myValidToken.equals(validateBean.getSignature())) {
            return validateBean.getEchostr();
        }
        return "";
    }
    
    public String signatureWxReq(String token, String timestamp, String nonce) {
        try {
            String[] array = new String[] { token, timestamp, nonce };
            StringBuffer sb = new StringBuffer();
            // 字符串排序
            Arrays.sort(array);
            for (int i = 0; i < 4; i++) {
                sb.append(array[i]);
            }
            String str = sb.toString();
            // SHA1签名生成
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(str.getBytes());
            byte[] digest = md.digest();

            StringBuffer hexstr = new StringBuffer();
            String shaHex = "";
            for (int i = 0; i < digest.length; i++) {
                shaHex = Integer.toHexString(digest[i] & 0xFF);
                if (shaHex.length() < 2) {
                    hexstr.append(0);
                }
                hexstr.append(shaHex);
            }
            return hexstr.toString();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("加密sign异常!");
        }
    }
    

按理肯定也不会有问题了。不过为了保险起见,我还是写了个测试用例!自测一下!

    private final String oldSysWxAddress = "http://a.com/wx";
    private final String newSysWxAddress = "http://localhost:8080/wx";

    @Test
    public void testWxValidateToken() throws IOException {

        String token = "abc123";
        String timestamp = new Date().getTime() / 1000 + "";
        String nonce = "207665";        // 这个就随便一写的数字
        String echoStr = "1kjdslfj";
        String signature = signatureWxReq(token, timestamp, nonce);

        Map<String, String> params = new HashMap<>();
        params.put("token", token);
        params.put("timestamp", timestamp);
        params.put("nonce", nonce);
        params.put("signature", signature);
        params.put("echostr", echoStr);

        String oldSysResponse = HttpClientOp.doGet(oldSysWxAddress, params);
        Assert.assertEquals("老系统验证不通过,请检查加密算法!", oldSysResponse, echoStr);

        String newSysResponse = HttpClientOp.doGet(newSysWxAddress, params);
        Assert.assertEquals("新系统验证不通过,出bug了!", newSysResponse, echoStr);

        Assert.assertEquals("新老返回不一致,测试不通过!", newSysResponse, oldSysResponse);
        System.out.println("OK");

    }
    

想着吧,也就走个过场得了。结果,还真不是这样!出问题了,"老系统验证不通过,请检查加密算法!" 。

按理不应该啊!但是代码是理智的,咱们得找到bug不是。

最后,通过一步步排查,终于发现了,原来是 php 的排序结果,与 java 的排序结果不一致,因此得到的加密串就不对了。

为啥呢?sort($tmpArr); php是弱类型语言,我们请求的虽然看起来是字符串,但是解析后,因为得到的是一整串数字,因此就认为是可以用整型或这种比较数字大小方式了。

所以,一比较时间和 nonce 随机数,因为随机数位数小,因此自然就应该排在时间戳的前面了。

而对于 java 的排序呢? Arrays.sort(Object[] a), 我们来看一下源码!

    public static void sort(Object[] a) {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a);
        else
            // 默认使用 ComparableTimSort 排序
            ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
    }
    
    // ComparableTimSort.sort()
    /**
     * Sorts the given range, using the given workspace array slice
     * for temp storage when possible. This method is designed to be
     * invoked from public methods (in class Arrays) after performing
     * any necessary array bounds checks and expanding parameters into
     * the required forms.
     *
     * @param a the array to be sorted
     * @param lo the index of the first element, inclusive, to be sorted
     * @param hi the index of the last element, exclusive, to be sorted
     * @param work a workspace array (slice)
     * @param workBase origin of usable space in work array
     * @param workLen usable size of work array
     * @since 1.8
     */
    static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {
        assert a != null && lo >= 0 && lo <= hi && hi <= a.length;

        int nRemaining  = hi - lo;
        if (nRemaining < 2)
            return;  // Arrays of size 0 and 1 are always sorted

        // If array is small, do a "mini-TimSort" with no merges
        if (nRemaining < MIN_MERGE) {
            // 二分插入排序
            int initRunLen = countRunAndMakeAscending(a, lo, hi);
            binarySort(a, lo, hi, lo + initRunLen);
            return;
        }

        /**
         * March over the array once, left to right, finding natural runs,
         * extending short natural runs to minRun elements, and merging runs
         * to maintain stack invariant.
         */
        ComparableTimSort ts = new ComparableTimSort(a, work, workBase, workLen);
        int minRun = minRunLength(nRemaining);
        do {
            // Identify next run
            int runLen = countRunAndMakeAscending(a, lo, hi);

            // If run is short, extend to min(minRun, nRemaining)
            if (runLen < minRun) {
                int force = nRemaining <= minRun ? nRemaining : minRun;
                binarySort(a, lo, lo + force, lo + runLen);
                runLen = force;
            }

            // Push run onto pending-run stack, and maybe merge
            ts.pushRun(lo, runLen);
            ts.mergeCollapse();

            // Advance to find next run
            lo += runLen;
            nRemaining -= runLen;
        } while (nRemaining != 0);

        // Merge all remaining runs to complete sort
        assert lo == hi;
        ts.mergeForceCollapse();
        assert ts.stackSize == 1;
    }


    /**
     * Returns the length of the run beginning at the specified position in
     * the specified array and reverses the run if it is descending (ensuring
     * that the run will always be ascending when the method returns).
     *
     * A run is the longest ascending sequence with:
     *
     *    a[lo] <= a[lo + 1] <= a[lo + 2] <= ...
     *
     * or the longest descending sequence with:
     *
     *    a[lo] >  a[lo + 1] >  a[lo + 2] >  ...
     *
     * For its intended use in a stable mergesort, the strictness of the
     * definition of "descending" is needed so that the call can safely
     * reverse a descending sequence without violating stability.
     *
     * @param a the array in which a run is to be counted and possibly reversed
     * @param lo index of the first element in the run
     * @param hi index after the last element that may be contained in the run.
              It is required that {@code lo < hi}.
     * @return  the length of the run beginning at the specified position in
     *          the specified array
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private static int countRunAndMakeAscending(Object[] a, int lo, int hi) {
        assert lo < hi;
        int runHi = lo + 1;
        if (runHi == hi)
            return 1;

        // Find end of run, and reverse range if descending
        // 调用的是 XXObject.compareTo() 方法,找出第一个比后续值小的index, 作为快排的基点
        if (((Comparable) a[runHi++]).compareTo(a[lo]) < 0) { // Descending
            while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) < 0)
                runHi++;
            reverseRange(a, lo, runHi);
        } else {                              // Ascending
            while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) >= 0)
                runHi++;
        }

        return runHi - lo;
    }
    
    /**
     * 反转元素
     * Reverse the specified range of the specified array.
     *
     * @param a the array in which a range is to be reversed
     * @param lo the index of the first element in the range to be reversed
     * @param hi the index after the last element in the range to be reversed
     */
    private static void reverseRange(Object[] a, int lo, int hi) {
        hi--;
        while (lo < hi) {
            Object t = a[lo];
            a[lo++] = a[hi];
            a[hi--] = t;
        }
    }
    
    
    /**
     * 二分插入排序
     * Sorts the specified portion of the specified array using a binary
     * insertion sort.  This is the best method for sorting small numbers
     * of elements.  It requires O(n log n) compares, but O(n^2) data
     * movement (worst case).
     *
     * If the initial part of the specified range is already sorted,
     * this method can take advantage of it: the method assumes that the
     * elements from index {@code lo}, inclusive, to {@code start},
     * exclusive are already sorted.
     *
     * @param a the array in which a range is to be sorted
     * @param lo the index of the first element in the range to be sorted
     * @param hi the index after the last element in the range to be sorted
     * @param start the index of the first element in the range that is
     *        not already known to be sorted ({@code lo <= start <= hi})
     */
    @SuppressWarnings({"fallthrough", "rawtypes", "unchecked"})
    private static void binarySort(Object[] a, int lo, int hi, int start) {
        assert lo <= start && start <= hi;
        if (start == lo)
            start++;
        for ( ; start < hi; start++) {
            Comparable pivot = (Comparable) a[start];

            // Set left (and right) to the index where a[start] (pivot) belongs
            int left = lo;
            int right = start;
            assert left <= right;
            /*
             * Invariants:
             *   pivot >= all in [lo, left).
             *   pivot <  all in [right, start).
             */
            while (left < right) {
                int mid = (left + right) >>> 1;
                if (pivot.compareTo(a[mid]) < 0)
                    right = mid;
                else
                    left = mid + 1;
            }
            assert left == right;

            /*
             * The invariants still hold: pivot >= all in [lo, left) and
             * pivot < all in [left, start), so pivot belongs at left.  Note
             * that if there are elements equal to pivot, left points to the
             * first slot after them -- that's why this sort is stable.
             * Slide elements over to make room for pivot.
             */
            int n = start - left;  // The number of elements to move
            // Switch is just an optimization for arraycopy in default case
            switch (n) {
                case 2:  a[left + 2] = a[left + 1];
                case 1:  a[left + 1] = a[left];
                         break;
                default: System.arraycopy(a, left, a, left + 1, n);
            }
            a[left] = pivot;
        }
    }

    
    // String.compareTo() 方法,比较 char大小,即字典顺序
    public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }

可以看到,Arrays.sort(), 对于小数的排序,是使用二分插入排序来做的,而具体排序先后,则是调用具体类的 compareTo() 方法,也就是说要进行比较的类,须实现 Comparator 接口。另外,从String的比较方法中,我们也可以看出 char 其实能保存很多东西而不只是 1-128。比如 char s = (char)"中";

而对于排序算法,则针对不同情况,选择不同的合适算法,从而提高运行效率!

而对于String.compareTo() 则是比较字符的ascii顺序!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK