微信公眾號接入之排序問題小記 Arrays.sort()


  微信公眾號作為強大的自媒體工具,對接一下是很正常的了。不過這不是本文的方向,本文的方向公眾號接入的排序問題。

  最近接了一個重構的小項目,需要將原有的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() 方法,也就是說要進行比較的類,須實現Comparable 接口。另外,從String的比較方法中,我們也可以看出 char 其實能保存很多東西而不只是 1-128。比如 char s = (char)"中"; 

      而對於排序算法,則針對不同情況,選擇不同的合適算法,從而提高運行效率!

      而對於String.compareTo() 則是比較字符的ascii順序!

算法說明:

1. 對於小數的排序,使用二分插入排序,原理:

  1. 先從最開始出發,找出最大的遞增或者遞減的個數,如果遞減,則倒序處理排列,從而使前 n 個元素呈排列狀態;

  2. 從之前排好序之后的元素開始,使用二分查找法,得到需要移動的個數,進行相應元素的轉換。因為 lo 一直是初始值,所以,在 start 循環到 hi 操作完成交換之后,就完成了所有的排序了。

2. 而對大些的數組排序,則使用分而治之的方法,拆分處理,原理:

  1. 先算出一個合適的大小,在將輸入按其升序和降序特點進行了分區,拆分的原則是大小需要小 MIN_MERGE,也就是32。

  2. 針對這些第個分區序列,先按照小數的排序方式排好,然后將結果按規則進行合並。

  3. 每次合並會將兩個run合並成一個 run。合並的結果保存到棧中。合並直到消耗掉所有的run,這時將棧上剩余的 run合並到只剩一個 run 為止。這時這個僅剩的 run 便是排好序的結果。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM