微信公眾號接入之排序問題小記
發 微信公眾號作為強大的自媒體工具,對接一下是很正常的了。不過這不是本文的方向,本文的方向公眾號接入的排序問題。
最近接了一個重構的小專案,需要將原有的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.compareTo() 則是比較字元的ascii順序!