很多人都说网易云音乐的一条评论是一个故事, 细细一看的话, 你会发现有些评论确实很真, 很性情。
抓取网易云音乐评论主要涉及了参数的加密问题,这篇博客主要是剖析一下如何进行参数的加密以及解密剖析
这里使用的是java进行加密实现数据的抓取。
爬虫有三难:
获取“电灯胆”歌词评论, 然后对歌词进行分词处理,获取高频词,制作词云图等
这里主要涉及的的技术点有:
图一 图二使用的是 图悦 进行分词统计并且生成词云图
http://www.picdata.cn/picdata/index.php#
图三使用的是java word 分词库进行分词, 结合数据库进行统计词频等生成的词云图
然后点击下一页, 网页会请求后台获取数据, 我们通过F12抓包可以知道链接地址以及参数等, 如下图:
我们可以了解到
url:https://music.163.com/weapi/v1/resource/comments/R_SO_4_1379057027?csrf_token=
method: post
content-type: application/x-www-form-urlencoded
从图一中可以知道发起请求的js是core_97f1bfe….js?97f1bfe
那么进入这个js查看其源码, 可以发现这个core_xxxx.js的东西竟然是一个4万5千多行的文件
不过不要慌, 如果真的去看完这个多代码都可以前端 js了, 我想这篇博客也是不知道猴年马月才可以写了
ctrl + f 搜索关键字 “encSecKey”
可以知道了生成参数params encSecKey的参数位置了
接着往上看两行代码
var bUP7I = window.asrsea(JSON.stringify(i8a), brF9w(["流泪", "强"]), brF9w(WZ4d.md), brF9w(["爱心", "女孩", "惊恐", "大笑"]));
发现是一个 window.asrsea的方法产生的一个对象bUP7I
在这里留一个心眼, brF9w 这是一个方法, 传递了一些乱七八糟的参数, 但是我在断点了好多次都发现是这几个奇奇怪怪的参数, 非常有意思的网易云音乐呀…
因此我们在这里可以理解为调用的window.asrsea()方法,
第一个参数是一个字符串,=>因为看到了JSON.stringify()这个方法
第二个参数是一个常量
第三个参数是一个常量
第四个参数是一个常量
接着我们ctrl + f 搜索关键字 “asrsea”
如下图, 我们这个五个方法进行了深入的了解以及剖析, 就是这几个方法花了我一个星期的空闲时间, 主要是不理解机密, 捣腾了
window.asrsea = d,
window.ecnonasr = e
可以了解到d方法赋值给window.asrsea, e赋值给window.ecnonasr
因此图四调用的window.asrsea, 实际上就是d方法
在12896行打一个断点, 我们查看一下四个参数究竟是什么东东
参数d:
{“rid”:“R_SO_4_1379057027”,“offset”:“100”,“total”:“false”,“limit”:“20”,“csrf_token”:""}
参数e:
010001
参数f:
00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
参数g:
0CoJUm6Qyw8W8jud
关键的源码
function a(a) {
var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
for (d = 0; a > d; d += 1)
e = Math.random() * b.length,
e = Math.floor(e),
c += b.charAt(e);
return c
}
function b(a, b) {
var c = CryptoJS.enc.Utf8.parse(b)
, d = CryptoJS.enc.Utf8.parse("0102030405060708")
, e = CryptoJS.enc.Utf8.parse(a)
, f = CryptoJS.AES.encrypt(e, c, {
iv: d,
mode: CryptoJS.mode.CBC
});
return f.toString()
}
function c(a, b, c) {
var d, e;
return setMaxDigits(131),
d = new RSAKeyPair(b,"",c),
e = encryptedString(d, a)
}
function d(d, e, f, g) {
var h = {}
, i = a(16);
return h.encText = b(d, g),
h.encText = b(h.encText, i),
h.encSecKey = c(i, e, f),
h
}
function e(a, b, d, e) {
var f = {};
return f.encText = c(a + e, b, d),
f
}
window.asrsea = d,
window.ecnonasr = e
结合步骤05的图解, 可以知道方法b是一个加密方法,方法d是一个参数组装方法,
细看方法d, 会发现调用了两次方法b, 也就是加密两次了, 第一次是对参数d, g加密, g是一个常量, 上面以及解析了, 第二次是对第一次机密的结果再次进行加密, 参数i是一个调用了a方法的16为随机数, 因此i也是可以理解为一个常量值, 通过debug, 就可以查到这个常量是什么了
接着再看 h.encSecKey = c(i, e, f),这行代码
由于 i 可以理解为常量, e 传入d方法的时候也是常量, f 传入d方法的时候也是常量,
因此可以理解为h.encSecKey也是一个常量, 我们只需要debug, 获取到一个h.encSecKey即可( 但是这个encSecKey和参数 i 需要 同一次debug出来的才可以, 否则有问题的…)
由于这里找了很多资料, 直接提供这个方法出来
https://blog.csdn.net/bicheng4769/article/details/80811676
https://zhidao.baidu.com/question/1819427615658816228.html
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.util.Base64Utils;
/**
* @description AESEncrypt加密工具
* @date 2019-08-24 23:51:19
* @author houyu [email protected]
*/
public class AESEncrypt {
public static String encrypt(String content, String sKey) {
/*
function b(a, b) {
var c = CryptoJS.enc.Utf8.parse(b)
, d = CryptoJS.enc.Utf8.parse("0102030405060708")
, e = CryptoJS.enc.Utf8.parse(a)
, f = CryptoJS.AES.encrypt(e, c, {
iv: d,
mode: CryptoJS.mode.CBC
});
return f.toString()
}
// 说明: 前段js没有使用填充模式, 默认使用了PKCS7Padding
//
// https://zhidao.baidu.com/question/1819427615658816228.html
// CryptoJS.enc.Utf8.parse方法才可以将key转为128bit的。好吧,既然说了是多次尝试,那么就不知道原因了,后期再对其进行更深入的研究。
// 字符串类型的key用之前需要用uft8先parse一下才能用
//
// 后端使用的是PKCS5Padding,但是在使用CryptoJS的时候发现根本没有这个偏移,查询后发现PKCS5Padding和PKCS7Padding是一样的东东,使用时默认就是按照PKCS7Padding进行偏移的
//
// CryptoJS.AES >> 算法
// CBC >> 模式
// 0102030405060708 >> 偏移量
//
// 由于CryptoJS生成的密文是一个对象,如果直接将其转为字符串是一个Base64编码过的,在encryptedData.ciphertext上的属性转为字符串才是后端需要的格式。
*/
try {
byte[] encryptedBytes;
byte[] byteContent = content.getBytes("UTF-8");
// 获取cipher对象,getInstance("算法/工作模式/填充模式")
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 采用AES方式将密码转化成密钥
SecretKeySpec secretKeySpec = new SecretKeySpec(sKey.getBytes(), "AES");
// 初始化偏移量
IvParameterSpec iv = new IvParameterSpec("0102030405060708".getBytes("UTF-8"));
//cipher对象初始化 init(“加密/解密,密钥,偏移量”)
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
//按照上面定义的方式对数据进行处理。
encryptedBytes = cipher.doFinal(byteContent);
return new String(Base64Utils.encode(encryptedBytes), "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
首先控制相同变量
参数d:
{“rid”:“R_SO_4_1379057027”,“offset”:“100”,“total”:“false”,“limit”:“20”,“csrf_token”:""}
参数g:
0CoJUm6Qyw8W8jud
浏览器控制台输入
b(’{“rid”:“R_SO_4_1379057027”,“offset”:“100”,“total”:“false”,“limit”:“20”,“csrf_token”:""}’, ‘0CoJUm6Qyw8W8jud’)
结果会报如下图错误
因此我们需要先定义方法b, 直接把方法b的源码丢到控制台即可
至此 以及实现了java后端加密和js加密一致的内容, 接下来就是比较简单的了,看一下封装对应前段的几个方法即可
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.util.Base64Utils;
/**
* @description AESEncrypt加密工具
* @date 2019-08-24 23:51:19
* @author houyu [email protected]
*/
public class AESEncrypt {
/**
* @description 对应网易云音乐的方法b
* @date 2019-08-25 00:05:05
* @author houyu [email protected]
*/
public static String encrypt(String content, String sKey) {
/*
function b(a, b) {
var c = CryptoJS.enc.Utf8.parse(b)
, d = CryptoJS.enc.Utf8.parse("0102030405060708")
, e = CryptoJS.enc.Utf8.parse(a)
, f = CryptoJS.AES.encrypt(e, c, {
iv: d,
mode: CryptoJS.mode.CBC
});
return f.toString()
}
// 说明: 前段js没有使用填充模式, 默认使用了PKCS7Padding
//
// https://zhidao.baidu.com/question/1819427615658816228.html
// CryptoJS.enc.Utf8.parse方法才可以将key转为128bit的。好吧,既然说了是多次尝试,那么就不知道原因了,后期再对其进行更深入的研究。
// 字符串类型的key用之前需要用uft8先parse一下才能用
//
// 后端使用的是PKCS5Padding,但是在使用CryptoJS的时候发现根本没有这个偏移,查询后发现PKCS5Padding和PKCS7Padding是一样的东东,使用时默认就是按照PKCS7Padding进行偏移的
//
// CryptoJS.AES >> 算法
// CBC >> 模式
// 0102030405060708 >> 偏移量
//
// 由于CryptoJS生成的密文是一个对象,如果直接将其转为字符串是一个Base64编码过的,在encryptedData.ciphertext上的属性转为字符串才是后端需要的格式。
*/
try {
byte[] encryptedBytes;
byte[] byteContent = content.getBytes("UTF-8");
// 获取cipher对象,getInstance("算法/工作模式/填充模式")
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 采用AES方式将密码转化成密钥
SecretKeySpec secretKeySpec = new SecretKeySpec(sKey.getBytes(), "AES");
// 初始化偏移量
IvParameterSpec iv = new IvParameterSpec("0102030405060708".getBytes("UTF-8"));
//cipher对象初始化 init(“加密/解密,密钥,偏移量”)
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
//按照上面定义的方式对数据进行处理。
encryptedBytes = cipher.doFinal(byteContent);
return new String(Base64Utils.encode(encryptedBytes), "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/** 以下参数是js debug获取到的 */
public static final String i = "C3Ba8sQUIHbPHC1Z";
public static final String e = "";
public static final String f = "";
public static final String g = "0CoJUm6Qyw8W8jud";
public static final String encSecKey = "7c23b7a80684cee814ecdc6252cc66e53a4df5890b1299783e5c575c709e7c8c22d98edc4074fc31b9bf7458e1ab6452c42fde55fcbd9b765f049da3809703686fc86b43ff757a2fa9eb77c0b04a51f02efb3e0ade116454561a6f2aefe89f6d611343383eaf643dce13b4ad1709ea2f8215f922c1a014d7fd79adcd0fa107a4";
/**
* @description 对应网易云音乐方法d, 其中参数d f g在外边定义好
* @date 2019-08-25 00:06:51
* @author houyu [email protected]
*/
public static Map<String, Object> methodD(String pageObject) {
String encText = encrypt(pageObject, g);
encText = encrypt(encText, i);// C3Ba8sQUIHbPHC1Z
// 这里就不必要调用方法c了,因为这里是一个固定值, 直接前段js debug出来即可
Map<String, Object> paramMap = new HashMap<>(2);
paramMap.put("params", encText);
paramMap.put("encSecKey", encSecKey);
return paramMap;
}
public static Map<String, Object> getParamMap(String rid, int pageIndex, int pageSize){
String s = "{\"rid\":\"%s\",\"offset\":\"%s\",\"total\":\"false\",\"limit\":\"%s\",\"csrf_token\":\"\"}";
s = String.format(s, rid, (pageIndex - 1) * pageSize, pageSize);
return methodD(s);
}
}
具体源码在上面的git
https://github.com/HouYuSource/spider/blob/master/src/main/java/cn/shaines/spider/module/music/Music163Spider.java
待完善的地方是:重试机制的建立, 也就是说某一页失败了,加入重新抓取队列中。
我的CSDN:https://blog.csdn.net/JinglongSource
我的博客:https://shaines.cn