本文是承接上一篇博客https://blog.csdn.net/bicheng4769/article/details/80802184中对网易云音乐中获取评论所需要的两个参数params和encSecKey的进一步分析和讨论。主要内容包括从一开始寻找参数,到如何获取参数加密的方式,到如何通过java实现加密算法的过程。
你能学到的内容:AES加密、抓包工具使用、java中如何使用AES。
本文利用java爬取该歌曲所有用户的评论数据。以歌曲周董的告白气球
为例,利用chrome F12查找获取评论的api,然后再基于AES算法对参数进行加密。最后通过webmagic成功获取歌曲的所有评论。本文部分算法参考知乎,全部代码在github地址上
进入https://music.163.com/#/song?id=418603077,F12打开DevTools工具页面,找到和评论相关的数据请求name为R_SO_4_418603077csrf_token=3e89b2e6840298e76bfc0c7dbfad1627的post请求,如下所示:
查看该请求的详细内容,request Header如下所示:
那么,最终我们需要获取的数据的api接口 以及参数都已经知道了
API:https://music.163.com/weapi/v1/resource/comments/R_SO_4_418603077csrf_token=137ee9288282cdeb180c395d0d4ca1d6
请求方式:POST
请求内容类型:application/x-www-form-urlencoded
请求参数:params、encSecKey
显示,我们下面主要任务就是要获取到params和encSecKey的值。从当前的值可以看出,这是加密后的内容,毫无疑问肯定是通过js加密的。而且,我们可以从第一张图的Initiator可以看出这两个参数是通过core.js这个js文件算出来。因此,我们下一步计划就是分析core.js的内容。
下载core.js之后,我们打开js文件,一共1.6w行。。。。。。。不过没关系,我们只需要关注参数出现的几行就ok。查找一下全文,我们可以看到params和encSeckey同时出现在这位置:
最终都是通过 windows.asrsea这个函数获取的,我们可以看到有4个参数,一脸懵逼。这是啥玩意儿,不过没关系,我们可以线上调试一下看看四个参数是什么内容。线上调试工具我使用filddler。原理就是将本地的js替换线上加载的js文件,这样就可以调试输出这4个参数值。本地js文件加上几行代码,如图所示:
打开fiddler,找到autoResponder,添加Rule,导入本地js文件最终页面如下图所示:
然后重新刷新页面,我们可以看到控制台已经有内容输出
。如下图所示:
第一个参数是一个json格式的:{rid: "R_SO_4_418603077",offset: "0",total: "true",limit: "20",csrf_token: ""}
rid的值就是R_SO_4 加上歌曲id;offset就是(评论页数-1)*20;total在第一页就是true,其余都是false。(大家可以根据不同的歌曲和翻页数多试几次)。
第二个参数:
010001
第三个参数:
00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
第四个参数:
0CoJUm6Qyw8W8jud
到此4个参数的值我们都已经知道了,直接用就可以了。下面的问题是 看看windows.asrsea是如何处理的。定位到这个函数,发现他其实就是叫d的函数,如下图所示:
我们一行一行的看吧。第一行算了,第二行i就是一个长度为16的随机的字符串,既然是随机的话,我们直接让i为16个F。
这个函数返回一个h对象,有2个属性 encText和encSeckey。很明显encText就是我们需要的params。我们可以看到params是经过b函数2次处理,ensSeckey是经过一次c函数处理,所以我们要看看b函数和c函数进行了什么操作,如图:
b函数其实就是一个AES
加密函数,一共经过2次加密:第一次加密:对d内容进行加密,key是第四个参数;第二次加密:对第一次加密的结果内容进行加密,key是i(也就是16个F)。而且在b函数中我们可以看到有个0102030405060708
,这是AES
加密算法的偏移量。模式是CBC模式。
c函数没看懂啥意思,但是看不懂没关系,我们从参数入手,c函数三个参数分别是i、第二个参数e(010001)、第三个参数是e。都是固定值,所以这个encSecKey对于我们来说是一个固定值,抄一个下来就可以直接用了。我这里i是16个f,所以encSecKey的值就是:
257348aecb5e556c066de214e531faadd1c55d814f9be95fd06d6bff9f4c7a41f831f6394d5a3fd2e3881736d94a02ca919d952872e7d0a50ebfa1769a7a62d512f5f1ca21aec60bc3819a9c3ffca5eca9a0dba6d6f7249b06f5965ecfff3695b54e1c28f3f624750ed39e7de08fc8493242e26dbc4484a01c76f739e135637c
直接用就ok。
几个知识点大家先了解一下:
AES(Advanced Encryption Standard)对称加密算法是一种高级数据加密标准,可有效抵制针对DES的攻击算法。特点:密钥建立时间短、灵敏性好、内存需求低、安全性高。AES使用固定长度的密钥,而不是变长密码。必须将密码转成密钥才能在AES中使用它们。
对称加密指的就是加密和解密使用同一个秘钥,所以叫做对称加密。对称加密只有一个秘钥,作为私钥。
常见的对称加密算法:DES,AES,3DES等等。
非对称加密指的是:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥。公钥加密的信息,只有私钥才能解密。私钥加密的信息,只有公钥才能解密。
常见的非对称加密算法:RSA,ECC
我们对密码加密不是一个个加密,而是分块加密,叫做密码块,工作模式就是对密码块进行处理的方式。
ECB:加密前根据加密块大小(如AES为128位)分成若干块,之后将每块使用相同的密钥单独加密,解密同理。
CBC:CBC模式对于每个待加密的密码块在加密前会先与前一个密码块的密文异或然后再用加密器加密。第一个明文块与一个叫初始化向量的数据块异或。
CFB:与ECB和CBC模式只能够加密块数据不同,CFB能够将块密文(Block Cipher)转换为流密文(Stream Cipher)。CFB的加密工作分为两部分:将一前段加密得到的密文再加密;再将第1步加密得到的数据与当前段的明文异或。
OFB:OFB是先用块加密器生成密钥流(Keystream),然后再将密钥流与明文流异或得到密文流,解密是先用块加密器生成密钥流,再将密钥流与密文流异或得到明文,由于异或操作的对称性所以加密和解密的流程是完全一样的。
在分组加密算法中,我们要对原文进行分组,然后再对分组内容进行加密,组装密文。其中有一步是分组,如何分组?假设现在数据长度不是16的整数倍,怎么分?所以,我们要对原文进行填充,将原文填充到16的整数倍。
NoPadding:不填充,如果原文不是16的整数倍就报错。
PKCS5Padding:数据少几个就填充几。
PaddingMode.Zeros:缺多少就补充多少个0。
当补充成16的整数倍后,还需要再补充16个字节,一定要比原来的多。
java内部已经给我们提供了加密解密的类。大部分都在 crypto
这个包下。主要用到Cipher
和SecretKeySpec
2个类。先上代码:
public static String AESEncrypt(String content,String sKey) {
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(ivParameter.getBytes());
//cipher对象初始化 init(“加密/解密,密钥,偏移量”)
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
//按照上面定义的方式对数据进行处理。
encryptedBytes = cipher.doFinal(byteContent);
return new String(Base64Utils.encode(encryptedBytes), "UTF-8");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
到此,加密算法基本完成,我们可以自己根据歌曲id构建对应的参数。
直接上代码:
/***
* 密钥
*/
private static String sKey = "0CoJUm6Qyw8W8jud";
@Override
public void process(Page page) {
Request request = new Request("https://music.163.com/weapi/v1/resource/comments/R_SO_4_" + songId + "?csrf_token=");
request.setMethod("post");
request.setRequestBody(HttpRequestBody.form(makePostParam(songId, "true", 1), "UTF-8"));
page.addTargetRequest(request);
}
/**
* @param songId 歌曲ID
* @param paging 是否第一页 true 第一页 其余传入false
* @param nowPageNum 当前页数
* @return
*/
public static String makeContent(String songId, String paging, int nowPageNum) {
int offset;
if (nowPageNum < 1) {
offset = 20;
}
offset = (nowPageNum - 1) * 20;
String baseContent = "{rid: \"R_SO_4_%s\",offset: \"%d\",total: \"%s\",limit: \"20\",csrf_token: \"\"}";
return String.format(baseContent, songId, offset, paging);
}
/**
* 获取评论的2个参数设置
*
* @param content
* @return
*/
public Map makePostParam(String content) {
Map map = new HashMap<>();
map.put("params", MusicEncrypt.AESEncrypt((MusicEncrypt.AESEncrypt(content, sKey)), "FFFFFFFFFFFFFFFF"));
map.put("encSecKey", MusicEncrypt.rsaEncrypt());
return map;
}
public Map makePostParam(String songId, String paging, int nowPageNum) {
return makePostParam(makeContent(songId, paging, nowPageNum));
}
最后总结一下我们对加密解析的思路:首先找到api,根据api的参数找到js的加密算法,了解到加密算法之后,利用java去实现算法, 然后利用算法构建参数,实现爬虫。
网上大部分对网易云音乐热评加密解析都是通过python实现,毕竟py做爬虫才是最正统的,奈何笔者不会python,只能通过java实现,写到最后,不禁感慨:还是多学点好耶。
以上所有的解析过程都参考了知乎一位美女的回答:有兴趣的同学可以看看平胸小仙女