58同城AES簽名介面分析
背景:需要獲取58同城上面釋出的職位資訊,其中的包括職位的招聘要求,薪資福利,公司的資訊,招聘者的聯絡方式。(中級爬蟲的難度係數)
- 職位詳情頁分析
某個職位詳情頁的連結
https://qy.m.58.com/m_detail/29379880488200/
開啟以上鍊接並且F12進入開發者模式
我們可以看見聯絡方式需要登陸後才可以檢視。
登陸後,右擊滑鼠檢視頁面的原始碼,發現html頁面並沒有電話號碼,這裡初步的猜測是通過ajax來載入渲染的(一般都是這種套路)
-
全域性搜尋分析
由上面可見聯絡方式所在的div塊是mobMsg和freecall,全域性對這兩個關鍵字做搜尋,一步一步走下去。
<div class="msgGui"> <h3>聯絡方式</h3> </div> <dl> <dt>聯絡電話:</dt> <dd class="mobMsg"> <div id="freecall"></div> </dd> <dt>電子郵箱:</dt> <dd>[email protected]</dd> <dt>公司網址:</dt> <dd class="bColr"> <a href="http://http://WWW.SHSHENXINGKEMAO.COM">http://http://WWW.SHSHENXINGKEMAO.COM</a> </dd> </dl>
</div>
可惜的是,這次沒有找到第二個mobMsg或者freecall關鍵字,當然啦,不是每一次全域性搜尋都是奏效的。
這裡繼續觀察其他的請求,上面也是猜測是ajax請求做渲染的,故需要將注意力移到XHR模組和JS模組。
在JS模組找到如下標識的請求,可以看見請求返回的內容有一個叫virtualNum欄位,顧名思義這個欄位的內容可能要和我們找到電話有關係。
請求的連結
https://zpservice.58.com/numberProtection/biz/enterprise/mBind/?uid=29379880488200&callback=jsonp_callback2
返回的內容
{"msg":"ok","code":"0","virtualNum":"1wSca13IEbrpJNlYBR3OEQ=="}
這次再根據virtualNum做一次全部搜尋。
這裡印證了我們上面說的ajax做請求並渲染頁面的做法,大多數的前端開發都是採用這種套路。
$.ajax({ type: "get", url: "//zpservice.58.com/numberProtection/biz/enterprise/mBind/?uid=" + userId + "&callback=?", dataType: "jsonp", success: function(data) { switch (data.code) { case "0": insertNum(data.virtualNum); break; case "4": $("#freecall").html("企業未公開"); break; case "2": $("#freecall").html('<a href="' + "//m.m.58.com/login/?path=" + window.location.href + '">登入後可檢視</a>'); break; case "3": case "6": insertNum(data.virtualNum); break; case "5": case "1": default: console.error(data.msg); break } }, error: function(err) { console.log(err) } })
由上面的JS我們可以知道前端頁面是根據後臺介面返回的結果做相應的操作,當code等於0和6的時候,就會呼叫insertNum()函式,那我們就繼續往下看看這個insertNum函式究竟在做什麼事情。
function insertNum(data) { $("#freecall").html(decrypt(data)) } function decrypt(word) { var key = CryptoJS.enc.Utf8.parse("5749812cr3412345"); var decrypt = CryptoJS.AES.decrypt(word, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); return CryptoJS.enc.Utf8.stringify(decrypt).toString() }
function insertNum(data)這個函式傳入剛才返回的virtualNum欄位的內容,對欄位的內容進行解密的操作。具體怎麼解密上面的程式碼一目瞭然。
關於CryptoJS請大家移步至如下傳送門:
https://cryptojs.gitbook.io/docs/ https://stackoverflow.com/questions/51005488/how-to-use-cryptojs-in-javascrip https://github.com/brix/crypto-js
從其官方的介紹中:
-
CryptoJS是 標準和安全密碼演算法的JavaScript實現
-
CryptoJS是使用最佳實踐和模式在JavaScript中實現的標準安全加密演算法的不斷增長的集合。它們速度很快,並且具有一致且簡單的介面。
-
CryptoJS說到底也就是js常用的安全密碼演算法的JS實現,如果對資料安全性有考慮的前端開發人員,那麼這個庫類都需要了解並且熟練使用。
綜合上面的分析我們知道58同城是用CryptoJS.AES.decrypt這個方法做電話號碼的加密解密。
-
後端先對原來真實的號碼做AES加密編碼
-
前端獲取得到加密的編碼根據加密的金鑰(這個金鑰現在是5749812cr3412345,上面截圖也可以看見)在進行AES解密即可得到電話號碼的原文
後端的java程式碼實現
//解密電話號碼 public String decodoTel(String html,Page page){ if (html.contains("must be login")){ //如果提示登陸則返回fail return "fail"; } html = html.substring(html.indexOf("{"),html.lastIndexOf("}")+1); JSONObject json = JSONObject.parseObject(html); String virtualNum = json.getString("virtualNum"); int code = json.getInteger("code"); String telNum = StringUtils.EMPTY; if (code==0||code ==6){ try { telNum = AESUtil.aesDecrypt(virtualNum, "5749812cr3412345"); //decrypKey這個金鑰58現在這個階段是這個,以後可能會變 } catch (Exception e) { e.printStackTrace(); logger.error("58tongcheng decrypKey has change").tag1("58tongcheng").tag2("decrypKey change").id(page.getRequest().getTrackId()).commit(); return "fail"; } } else if (code == 2){ logger.error("58tongcheng need login").tag1("58tongcheng").tag2("need login").id(page.getRequest().getTrackId()).commit(); } return telNum; }
核心AES程式碼
package com.gemdata.crawler.generic.util; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; /** * AES的加密和解密*/ public class AESUtil { //金鑰 (需要前端和後端保持一致) private static final String KEY = "5749812cr3412345"; //現在這階段這個金鑰是58同城的,以後如果遇到新的一個加密解密方法,自行修改 //演算法 private static final String ALGORITHMSTR = "AES/ECB/PKCS5Padding"; /** * aes解密 * @param encrypt 內容 * @return * @throws Exception */ public static String aesDecrypt(String encrypt) { try { return aesDecrypt(encrypt, KEY); } catch (Exception e) { e.printStackTrace(); return ""; } } /** * aes加密 * @param content * @return * @throws Exception */ public static String aesEncrypt(String content) { try { return aesEncrypt(content, KEY); } catch (Exception e) { e.printStackTrace(); return ""; } } /** * 將byte[]轉為各種進位制的字串 * @param bytes byte[] * @param radix 可以轉換進位制的範圍,從Character.MIN_RADIX到Character.MAX_RADIX,超出範圍後變為10進位制 * @return 轉換後的字串 */ public static String binary(byte[] bytes, int radix){ return new BigInteger(1, bytes).toString(radix);// 這裡的1代表正數 } /** * base 64 encode * @param bytes 待編碼的byte[] * @return 編碼後的base 64 code */ public static String base64Encode(byte[] bytes){ return Base64.encodeBase64String(bytes); } /** * base 64 decode * @param base64Code 待解碼的base 64 code * @return 解碼後的byte[] * @throws Exception */ public static byte[] base64Decode(String base64Code) throws Exception{ //return StringUtils.isEmpty(base64Code) ? null : new BASE64Decoder().decodeBuffer(base64Code); return StringUtils.isEmpty(base64Code) ? null : new Base64().decodeBase64(base64Code); } /** * AES加密 * @param content 待加密的內容 * @param encryptKey 加密金鑰 * @return 加密後的byte[] * @throws Exception */ public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception { KeyGenerator kgen = KeyGenerator.getInstance("AES"); kgen.init(128); Cipher cipher = Cipher.getInstance(ALGORITHMSTR); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES")); return cipher.doFinal(content.getBytes("utf-8")); } /** * AES加密為base 64 code * @param content 待加密的內容 * @param encryptKey 加密金鑰 * @return 加密後的base 64 code * @throws Exception */ public static String aesEncrypt(String content, String encryptKey) throws Exception { return base64Encode(aesEncryptToBytes(content, encryptKey)); } /** * AES解密 * @param encryptBytes 待解密的byte[] * @param decryptKey 解密金鑰 * @return 解密後的String * @throws Exception */ public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception { KeyGenerator kgen = KeyGenerator.getInstance("AES"); kgen.init(128); Cipher cipher = Cipher.getInstance(ALGORITHMSTR); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), "AES")); byte[] decryptBytes = cipher.doFinal(encryptBytes); return new String(decryptBytes); } /** * 將base 64 code AES解密 * @param encryptStr 待解密的base 64 code * @param decryptKey 解密金鑰 * @return 解密後的string * @throws Exception */ public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception { return StringUtils.isEmpty(encryptStr) ? null : aesDecryptByBytes(base64Decode(encryptStr), decryptKey); } //MD5摘要 public static String MD5(String sourceStr) { String result = ""; try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(sourceStr.getBytes()); byte b[] = md.digest(); int i; StringBuffer buf = new StringBuffer(""); for (int offset = 0; offset < b.length; offset++) { i = b[offset]; if (i < 0) i += 256; if (i < 16) buf.append("0"); buf.append(Integer.toHexString(i)); } result = buf.toString(); } catch (NoSuchAlgorithmException e) { System.out.println(e); } return result; } /** * 測試 */ public static void main(String[] args) throws Exception { String content = "13044154254"; System.out.println("加密前:" + content); System.out.println("加密金鑰和解密金鑰:" + KEY); String encrypt = aesEncrypt(content, KEY); System.out.println("加密後:" + encrypt); String decrypt = aesDecrypt(encrypt, "5749812cr3412345"); System.out.println("解密後:" + decrypt); } }
其中AES解碼的程式碼段,參考瞭如下的連結。
https://www.chenwenguan.com/aes-encryption-decryption
最後的結果如下,直接貼上上面的程式碼執行即可。
本文首發於本人的公眾號,需要轉載請把原文連結帶上
https://mp.weixin.qq.com/s?__biz=MzIyNTcwMzA5NQ==&mid=2247483852&idx=1&sn=ac3903d00679779d5457c2785e0083b3&chksm=e87ae414df0d6d024107f375eb29bff075170101cdafecb8c945e5d725447f2db262d43ee3f9&token=797684618&lang=zh_CN#rd
關於呼呼:會點爬蟲,會點後端,會點前端,會點逆向,會點資料分析,會點演算法,一個喜歡陳奕迅的