背景:需要获取58同城上面发布的职位信息,其中的包括职位的招聘要求,薪资福利,公司的信息,招聘者的联系方式。(中级爬虫的难度系数)

  • 职位详情页分析

某个职位详情页的链接

  1. https://qy.m.58.com/m_detail/29379880488200/

打开以上链接并且F12进入开发者模式

我们可以看见联系方式需要登陆后才可以查看。

登陆后,右击鼠标查看页面的源码,发现html页面并没有电话号码,这里初步的猜测是通过ajax来加载渲染的(一般都是这种套路)

  • 全局搜索分析

由上面可见联系方式所在的div块是mobMsg和freecall,全局对这两个关键字做搜索,一步一步走下去。

  1. <div class="msgGui">
  2. <h3>联系方式</h3>
  3. </div>
  4. <dl>
  5. <dt>联系电话:</dt>
  6. <dd class="mobMsg">
  7. <div id="freecall"></div>
  8. </dd>
  9. <dt>电子邮箱:</dt>
  10. <dd>456151546@QQ.COM</dd>
  11. <dt>公司网址:</dt>
  12. <dd class="bColr">
  13. <a href="http://http://WWW.SHSHENXINGKEMAO.COM">http://http://WWW.SHSHENXINGKEMAO.COM</a>
  14. </dd>
  15. </dl></div>

  

可惜的是,这次没有找到第二个mobMsg或者freecall关键字,当然啦,不是每一次全局搜索都是奏效的。

这里继续观察其他的请求,上面也是猜测是ajax请求做渲染的,故需要将注意力移到XHR模块和JS模块。

在JS模块找到如下标识的请求,可以看见请求返回的内容有一个叫virtualNum字段,顾名思义这个字段的内容可能要和我们找到电话有关系。

请求的链接

  1. https://zpservice.58.com/numberProtection/biz/enterprise/mBind/?uid=29379880488200&callback=jsonp_callback2

返回的内容

  1. {","virtualNum":"1wSca13IEbrpJNlYBR3OEQ=="}

这次再根据virtualNum做一次全部搜索。

这里印证了我们上面说的ajax做请求并渲染页面的做法,大多数的前端开发都是采用这种套路。

  1. $.ajax({
  2. type: "get",
  3. url: "//zpservice.58.com/numberProtection/biz/enterprise/mBind/?uid=" + userId + "&callback=?",
  4. dataType: "jsonp",
  5. success: function(data) {
  6. switch (data.code) {
  7. ":
  8. insertNum(data.virtualNum);
  9. break;
  10. ":
  11. $("#freecall").html("企业未公开");
  12. break;
  13. ":
  14. $("#freecall").html('<a href="' + "//m.m.58.com/login/?path=" + window.location.href + '">登录后可查看</a>');
  15. break;
  16. ":
  17. ":
  18. insertNum(data.virtualNum);
  19. break;
  20. ":
  21. ":
  22. default:
  23. console.error(data.msg);
  24. break
  25. }
  26. },
  27. error: function(err) {
  28. console.log(err)
  29. }
  30. })

由上面的JS我们可以知道前端页面是根据后台接口返回的结果做相应的操作,当code等于0和6的时候,就会调用insertNum()函数,那我们就继续往下看看这个insertNum函数究竟在做什么事情。

  1. function insertNum(data) {
  2. $("#freecall").html(decrypt(data))
  3. }
  4. function decrypt(word) {
  5. var key = CryptoJS.enc.Utf8.parse("5749812cr3412345");
  6. var decrypt = CryptoJS.AES.decrypt(word, key, {
  7. mode: CryptoJS.mode.ECB,
  8. padding: CryptoJS.pad.Pkcs7
  9. });
  10. return CryptoJS.enc.Utf8.stringify(decrypt).toString()
  11. }

 

function insertNum(data)这个函数传入刚才返回的virtualNum字段的内容,对字段的内容进行解密的操作。具体怎么解密上面的代码一目了然。

关于CryptoJS请大家移步至如下传送门:

  1. https://cryptojs.gitbook.io/docs/
  2. https://stackoverflow.com/questions/51005488/how-to-use-cryptojs-in-javascrip
  3. https://github.com/brix/crypto-js

  

从其官方的介绍中:

  • CryptoJS是 标准和安全密码算法的JavaScript实现

  • CryptoJS是使用最佳实践和模式在JavaScript中实现的标准安全加密算法的不断增长的集合。它们速度很快,并且具有一致且简单的界面。

  • CryptoJS说到底也就是js常用的安全密码算法的JS实现,如果对数据安全性有考虑的前端开发人员,那么这个库类都需要了解并且熟练使用。

综合上面的分析我们知道58同城是用CryptoJS.AES.decrypt这个方法做电话号码的加密解密。

  • 后端先对原来真实的号码做AES加密编码

  • 前端获取得到加密的编码根据加密的密钥(这个密钥现在是5749812cr3412345,上面截图也可以看见)在进行AES解密即可得到电话号码的原文

后端的java代码实现

  1. //解密电话号码
  2. public String decodoTel(String html,Page page){
  3.  
  4. if (html.contains("must be login")){ //如果提示登陆则返回fail
  5. return "fail";
  6. }
  7. html = html.substring(html.indexOf("{"),html.lastIndexOf("}")+1);
  8. JSONObject json = JSONObject.parseObject(html);
  9. String virtualNum = json.getString("virtualNum");
  10. int code = json.getInteger("code");
  11. String telNum = StringUtils.EMPTY;
  12.  
  13. if (code==0||code ==6){
  14. try {
  15. telNum = AESUtil.aesDecrypt(virtualNum, "5749812cr3412345"); //decrypKey这个密钥58现在这个阶段是这个,以后可能会变
  16. } catch (Exception e) {
  17. e.printStackTrace();
  18. logger.error("58tongcheng decrypKey has change").tag1("58tongcheng").tag2("decrypKey change").id(page.getRequest().getTrackId()).commit();
  19. return "fail";
  20. }
  21. }
  22. else if (code == 2){
  23. logger.error("58tongcheng need login").tag1("58tongcheng").tag2("need login").id(page.getRequest().getTrackId()).commit();
  24. }
  25.  
  26. return telNum;
  27. }

核心AES代码

  1. package com.gemdata.crawler.generic.util;
  2.  
  3. import java.math.BigInteger;
  4. import java.security.MessageDigest;
  5. import java.security.NoSuchAlgorithmException;
  6.  
  7. import javax.crypto.Cipher;
  8. import javax.crypto.KeyGenerator;
  9. import javax.crypto.spec.SecretKeySpec;
  10.  
  11. import org.apache.commons.codec.binary.Base64;
  12. import org.apache.commons.lang3.StringUtils;
  13.  
  14. /**
  15. * AES的加密和解密*/
  16. public class AESUtil {
  17. //密钥 (需要前端和后端保持一致)
  18. private static final String KEY = "5749812cr3412345"; //现在这阶段这个密钥是58同城的,以后如果遇到新的一个加密解密方法,自行修改
  19. //算法
  20. private static final String ALGORITHMSTR = "AES/ECB/PKCS5Padding";
  21.  
  22. /**
  23. * aes解密
  24. * @param encrypt 内容
  25. * @return
  26. * @throws Exception
  27. */
  28. public static String aesDecrypt(String encrypt) {
  29. try {
  30. return aesDecrypt(encrypt, KEY);
  31. } catch (Exception e) {
  32. e.printStackTrace();
  33. return "";
  34. }
  35. }
  36.  
  37. /**
  38. * aes加密
  39. * @param content
  40. * @return
  41. * @throws Exception
  42. */
  43. public static String aesEncrypt(String content) {
  44. try {
  45. return aesEncrypt(content, KEY);
  46. } catch (Exception e) {
  47. e.printStackTrace();
  48. return "";
  49. }
  50. }
  51.  
  52. /**
  53. * 将byte[]转为各种进制的字符串
  54. * @param bytes byte[]
  55. * @param radix 可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制
  56. * @return 转换后的字符串
  57. */
  58. public static String binary(byte[] bytes, int radix){
  59. return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数
  60. }
  61.  
  62. /**
  63. * base 64 encode
  64. * @param bytes 待编码的byte[]
  65. * @return 编码后的base 64 code
  66. */
  67. public static String base64Encode(byte[] bytes){
  68. return Base64.encodeBase64String(bytes);
  69. }
  70.  
  71. /**
  72. * base 64 decode
  73. * @param base64Code 待解码的base 64 code
  74. * @return 解码后的byte[]
  75. * @throws Exception
  76. */
  77. public static byte[] base64Decode(String base64Code) throws Exception{
  78. //return StringUtils.isEmpty(base64Code) ? null : new BASE64Decoder().decodeBuffer(base64Code);
  79. return StringUtils.isEmpty(base64Code) ? null : new Base64().decodeBase64(base64Code);
  80.  
  81. }
  82.  
  83. /**
  84. * AES加密
  85. * @param content 待加密的内容
  86. * @param encryptKey 加密密钥
  87. * @return 加密后的byte[]
  88. * @throws Exception
  89. */
  90. public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception {
  91. KeyGenerator kgen = KeyGenerator.getInstance("AES");
  92. kgen.init(128);
  93. Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
  94. cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES"));
  95.  
  96. return cipher.doFinal(content.getBytes("utf-8"));
  97. }
  98.  
  99. /**
  100. * AES加密为base 64 code
  101. * @param content 待加密的内容
  102. * @param encryptKey 加密密钥
  103. * @return 加密后的base 64 code
  104. * @throws Exception
  105. */
  106. public static String aesEncrypt(String content, String encryptKey) throws Exception {
  107. return base64Encode(aesEncryptToBytes(content, encryptKey));
  108. }
  109.  
  110. /**
  111. * AES解密
  112. * @param encryptBytes 待解密的byte[]
  113. * @param decryptKey 解密密钥
  114. * @return 解密后的String
  115. * @throws Exception
  116. */
  117. public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception {
  118. KeyGenerator kgen = KeyGenerator.getInstance("AES");
  119. kgen.init(128);
  120.  
  121. Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
  122. cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), "AES"));
  123. byte[] decryptBytes = cipher.doFinal(encryptBytes);
  124. return new String(decryptBytes);
  125. }
  126.  
  127. /**
  128. * 将base 64 code AES解密
  129. * @param encryptStr 待解密的base 64 code
  130. * @param decryptKey 解密密钥
  131. * @return 解密后的string
  132. * @throws Exception
  133. */
  134. public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception {
  135. return StringUtils.isEmpty(encryptStr) ? null : aesDecryptByBytes(base64Decode(encryptStr), decryptKey);
  136. }
  137.  
  138. //MD5摘要
  139. public static String MD5(String sourceStr)
  140. {
  141. String result = "";
  142. try
  143. {
  144. MessageDigest md = MessageDigest.getInstance("MD5");
  145. md.update(sourceStr.getBytes());
  146. byte b[] = md.digest();
  147. int i;
  148. StringBuffer buf = new StringBuffer("");
  149. for (int offset = 0; offset < b.length; offset++)
  150. {
  151. i = b[offset];
  152. if (i < 0)
  153. i += 256;
  154. if (i < 16)
  155. buf.append("0");
  156. buf.append(Integer.toHexString(i));
  157. }
  158. result = buf.toString();
  159. } catch (NoSuchAlgorithmException e)
  160. {
  161. System.out.println(e);
  162. }
  163. return result;
  164. }
  165.  
  166. /**
  167. * 测试
  168. */
  169. public static void main(String[] args) throws Exception {
  170. String content = "13044154254";
  171. System.out.println("加密前:" + content);
  172. System.out.println("加密密钥和解密密钥:" + KEY);
  173. String encrypt = aesEncrypt(content, KEY);
  174. System.out.println("加密后:" + encrypt);
  175. String decrypt = aesDecrypt(encrypt, "5749812cr3412345");
  176. System.out.println("解密后:" + decrypt);
  177. }
  178. }

其中AES解码的代码段,参考了如下的链接。

  1. https://www.chenwenguan.com/aes-encryption-decryption

最后的结果如下,直接贴上上面的代码运行即可。

本文首发于本人的公众号,需要转载请把原文链接带上

  1. https://mp.weixin.qq.com/s?__biz=MzIyNTcwMzA5NQ==&mid=2247483852&idx=1&sn=ac3903d00679779d5457c2785e0083b3&chksm=e87ae414df0d6d024107f375eb29bff075170101cdafecb8c945e5d725447f2db262d43ee3f9&token=797684618&lang=zh_CN#rd

  

关于呼呼:会点爬虫,会点后端,会点前端,会点逆向,会点数据分析,会点算法,一个喜欢陈奕迅的

58同城AES签名接口分析的更多相关文章

  1. 转载:MongoDB 在 58 同城百亿量级数据下的应用实践

    为什么要使用 MongoDB? MongoDB 这个来源英文单词“humongous”,homongous 这个单词的意思是“巨大的”.“奇大无比的”,从 MongoDB 单词本身可以看出它的目标是提 ...

  2. [MISSAJJ原创] UITableViewCell移动及翻转出现的3D动画效果[58同城cell移动效果]

    2015-11-20 很喜欢在安静的状态, 听着音乐,敲着键盘, 和代码们浓情对话, 每一份代码的积累, 都让自己觉得很充实快乐!Y(^_^)Y. 看到58同城app的cell有动画移动出现的特效,很 ...

  3. 用Python写爬虫爬取58同城二手交易数据

    爬了14W数据,存入Mongodb,用Charts库展示统计结果,这里展示一个示意 模块1 获取分类url列表 from bs4 import BeautifulSoup import request ...

  4. 58同城高性能移动Push推送平台架构演进之路

    本文详细讲述58同城高性能移动Push推送平台架构演进的三个阶段,并介绍了什么是移动Push推送,为什么需要,原理和方案对比:移动Push推送第一阶段(单平台)架构如何设计:移动Push推送典型性能问 ...

  5. 养只爬虫当宠物(Node.js爬虫爬取58同城租房信息)

    先上一个源代码吧. https://github.com/answershuto/Rental 欢迎指导交流. 效果图 搭建Node.js环境及启动服务 安装node以及npm,用express模块启 ...

  6. 【Android测试】【随笔】与 “58同城” 测试开发交流

    ◆版权声明:本文出自胖喵~的博客,转载必须注明出处. 转载请注明出处:http://www.cnblogs.com/by-dream/p/5384698.html 初衷 一直都有一个这样的想法: 虽然 ...

  7. 转: 58同城高性能移动Push推送平台架构演进之路

    转: http://geek.csdn.net/news/detail/58738 文/孙玄 本文详细讲述58同城高性能移动Push推送平台架构演进的三个阶段,并介绍了什么是移动Push推送,为什么需 ...

  8. ios loading视图动画(模仿58同城)

    最近看了58同城的加载视图,感觉很不错,如下图: 所以想模仿写一个,下载58同城的app,解压,发现它用的是图片来实现的动画效果, 并不是绘制出来的,所以这就相对简单些了,其实整个动画的逻辑不复杂,无 ...

  9. python3.4+pyspider爬58同城(二)

    之前使用python3.4+selenium实现了爬58同城的详细信息,这次用pyspider实现,网上搜了下,目前比较流行的爬虫框架就是pyspider和scrapy,但是scrapy不支持pyth ...

随机推荐

  1. 调度系统Airflow的第一个DAG

    Airflow的第一个DAG 考虑了很久,要不要记录airflow相关的东西, 应该怎么记录. 官方文档已经有比较详细的介绍了,还有各种博客,我需要有一份自己的笔记吗? 答案就从本文开始了. 本文将从 ...

  2. Linux之acl库的安装与使用(限制Linux某用户的访问权限)

    acl库 作用:限制Linux某用户的访问权限 acl库的安装 首先github中下载acl代码: git clone https://github.com/acl-dev/acl 进入acl, 执行 ...

  3. css 元素实际宽高

    首先定义一个div. 然后稍微装修一下 下面开始区分 一.clientWidth和clientHeigh . clientTop和clientLeft 1,clientWidth的实际宽度 clien ...

  4. ASP.NET MVC实现依赖注入

    在java的spring中有自动注入功能,使得代码变得更加简洁灵活,所以想把这个功能移植到c#中,接下来逐步分析实现过程 1.使用自动注入场景分析 在asp.net mvc中,无论是什么代码逻辑分层, ...

  5. 当React开发者初次走进React-Native的世界

    RN千机变 1.技术体系问题 RN和React共用一套抽象层,相对于前端,RN其实更接近Node的运行环境 ReactNative =React +IOS +Android 看RN文档时,我会发现入门 ...

  6. UI自动化测试养成记

    <selenium自动化测试实战>PDF文档下载:https://pan.baidu.com/s/16dt8qPi-C4BOgKe6snAA0A 这几个月我都干了些什么? 当我打算写一本& ...

  7. 062 Python必备库-从Web解析到网络空间

    目录 一.概述 二.Python库之网络爬虫 2.1 Requests 2.2 Scrapy 2.3 pyspider 三.Python库之Web信息提取 3.1 Beautiful Soup 3.2 ...

  8. JS 转换日期UTC类型

    前台取到的日期类型为UTC,"yyyy-MM-dd'T'HH:mm:ss.SSS",后台接收报错如下: org.springframework.http.converter.Htt ...

  9. 巨杉Tech | Hbase迁移至SequoiaDB 实战

    背景 在传统银行 IT 架构中,联机交易与统计分析系统往往采用不同的技术与物理设备,通过定期执行的 ETL 将联机交易数据向分析系统中迁移.而作为数据服务资源池,同一份数据可能被不同类型的微服务共享访 ...

  10. SqlServer 2014 还原数据库时提示:操作系统返回了错误5,,拒绝访问

    场景 在进行数据库还原时提示: System.Data.SqlError:在对”“尝试”“时,操作系统返回了错误5(拒绝访问) 实现 第一种方案是修改要还原的数据库备份文件的权限. 找到备份文件右击属 ...