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>456151546@QQ.COM</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
返回的内容
{","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) {
":
insertNum(data.virtualNum);
break;
":
$("#freecall").html("企业未公开");
break;
":
$("#freecall").html('<a href="' + "//m.m.58.com/login/?path=" + window.location.href + '">登录后可查看</a>');
break;
":
":
insertNum(data.virtualNum);
break;
":
":
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
关于呼呼:会点爬虫,会点后端,会点前端,会点逆向,会点数据分析,会点算法,一个喜欢陈奕迅的
58同城AES签名接口分析的更多相关文章
- 转载:MongoDB 在 58 同城百亿量级数据下的应用实践
为什么要使用 MongoDB? MongoDB 这个来源英文单词“humongous”,homongous 这个单词的意思是“巨大的”.“奇大无比的”,从 MongoDB 单词本身可以看出它的目标是提 ...
- [MISSAJJ原创] UITableViewCell移动及翻转出现的3D动画效果[58同城cell移动效果]
2015-11-20 很喜欢在安静的状态, 听着音乐,敲着键盘, 和代码们浓情对话, 每一份代码的积累, 都让自己觉得很充实快乐!Y(^_^)Y. 看到58同城app的cell有动画移动出现的特效,很 ...
- 用Python写爬虫爬取58同城二手交易数据
爬了14W数据,存入Mongodb,用Charts库展示统计结果,这里展示一个示意 模块1 获取分类url列表 from bs4 import BeautifulSoup import request ...
- 58同城高性能移动Push推送平台架构演进之路
本文详细讲述58同城高性能移动Push推送平台架构演进的三个阶段,并介绍了什么是移动Push推送,为什么需要,原理和方案对比:移动Push推送第一阶段(单平台)架构如何设计:移动Push推送典型性能问 ...
- 养只爬虫当宠物(Node.js爬虫爬取58同城租房信息)
先上一个源代码吧. https://github.com/answershuto/Rental 欢迎指导交流. 效果图 搭建Node.js环境及启动服务 安装node以及npm,用express模块启 ...
- 【Android测试】【随笔】与 “58同城” 测试开发交流
◆版权声明:本文出自胖喵~的博客,转载必须注明出处. 转载请注明出处:http://www.cnblogs.com/by-dream/p/5384698.html 初衷 一直都有一个这样的想法: 虽然 ...
- 转: 58同城高性能移动Push推送平台架构演进之路
转: http://geek.csdn.net/news/detail/58738 文/孙玄 本文详细讲述58同城高性能移动Push推送平台架构演进的三个阶段,并介绍了什么是移动Push推送,为什么需 ...
- ios loading视图动画(模仿58同城)
最近看了58同城的加载视图,感觉很不错,如下图: 所以想模仿写一个,下载58同城的app,解压,发现它用的是图片来实现的动画效果, 并不是绘制出来的,所以这就相对简单些了,其实整个动画的逻辑不复杂,无 ...
- python3.4+pyspider爬58同城(二)
之前使用python3.4+selenium实现了爬58同城的详细信息,这次用pyspider实现,网上搜了下,目前比较流行的爬虫框架就是pyspider和scrapy,但是scrapy不支持pyth ...
随机推荐
- DB2 根据id查表
SELECT * FROM SYSCAT.TABLES WHERE TBSPACEID = 2 AND TABLEID = 50 SELECT * FROM SYSCAT.COLUMNS WHERE ...
- luogu- P1373 小a和uim之大逃离 DP 四维,其中一维记录差值
P1373 小a和uim之大逃离: https://www.luogu.org/problemnew/show/P1373 题意: 在一个矩阵中,小A和小B轮流取数,小A可以从任意点先取,小B后取,最 ...
- Codeforces Round #479 (Div. 3) B. Two-gram
原题代码:http://codeforces.com/contest/977/problem/B 题解:有n个字符组成的字符串,输出出现次数两个字符组合.例如第二组样例ZZ出现了两次. 方法:比较无脑 ...
- Atcode B - Colorful Hats(思维)
题目链接:http://agc016.contest.atcoder.jp/tasks/agc016_b 题解:挺有意思的题目主要还是模拟出最多有几种不可能的情况,要知道ai的差距不能超过1这个想想就 ...
- NOIP 2016 组合数问题 题解
一道sb题目,注意范围,可打表解决,打出杨辉三角,在用前缀和求解即可 代码(一维前缀和) #include<bits/stdc++.h> using namespace std; int ...
- 小白学Python-S3-day04-用户信息的增删改查、变更权限
一.用户信息 文件中每一行就是用户的详细信息,每一行是按照冒号为分隔符分成七段 第一段用户名,第二段密码占位符,第三段UID,第四段GID,第五段是描述信息,第六段是家目录.第七段是 是否 可以登录操 ...
- Redis字符串键的底层原理
before C语言基础 Redis基础 导入 redis的命令如下: set x "hello"; get x; hello Redis作为一种存储字符串的缓存结构,其具体实现是 ...
- sql 删除完全表中完全重复的数据保留一条
1.删除完全重复数据 原始数据: 期望数据: delete result from (select ROW_NUMBER () over(partition by id order by id) r, ...
- 对android中ActionBar中setDisplayHomeAsUpEnabled和setHomeButtonEnabled和setDisplayShowHomeEnabled方法的理解
转自: http://blog.csdn.net/lovexieyuan520/article/details/9974929 http://blog.csdn.net/cyp331203/artic ...
- idea解决This file is indented with tabs instead of 4 spaces
idea上面总是弹出 解决方法: File -> Settings -> Editor -> Code Style -> Java -> Tabs and Indents ...