最近研究了爬虫技术 首先就盯上了网易云(因为我接触的第一个爬虫项目就是网易云接口
在网上搜了些教程后发现爬虫入门还是比较简单的 只需要有一些web基础(加上会写代码
于是我开始抓取网易云的歌曲播放接口流程也很简单直接登录网页版网易云随便点个歌进去但播放抓包即可
分析接口
这里使用的是Chrome浏览器 因为调试方便
点进去后首先让我注意到的是这个网址看上去就不简单
后面的这个id一看就感觉很像网易云用来识别歌曲的东西 于是我开始抓包查看请求体里是否有这个id 如果有那多半就是这个接口了
经过了漫长的抓取发现 找不到... 于是我开始换思路 我自己写接口时都会对参数进行加密 网易这么大的公司怎么可能不加密
知道了这个后还是无从下手 就算我知道了他的参数加密了 但我又怎么知道哪个接口是播放的 这些加密的内容根本都长一个样子
于是我又换了一个思路 竟然请求体看不出来 那我从响应体中能不能找到呢
最终让我发现一下特别像的接口
这个叫v1的接口 他的响应体里面可以看到有歌曲的id和一个url 我直接访问这个url试试看对不对 还真让我找到了
找到了播放接口后还有一个问题 他这个接口的请求方法是POST 在看了请求体的内容后发现这个参数是被加密的
分析加密
这个加密的内容换谁也看不懂 但是可以知道他的请求类型是from表单 并且有两个参数 于是我开始在js代码中搜索这两个参数(Ctrl+F) 找到了这个文件
这段js代码看上去确实很像加密 于是我开始打断点调试 可以确定这段代码确实就是加密
搜索了一番后找到了这个函数
分析代码后知道了 i就是一个随机的字符串 而encText是用text进行AES以CBC模式加密了两次的结果
第一次加密加密text key为g 第二次加密text是第一次加密后得到的密文 key为i
两次加密的iv都是0102030405060708
而encSecKey是一个RSA加密
其中参数d就是请求的参数 而后面的几个参数的值都是固定的 知道了这个后来用Java复写就很简单了
使用Java进行复写
/**
* 网易云音乐 工具
*
* @author 白
*/
public class Music163util {
private static final String modulus = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7";
private static final String nonce = "0CoJUm6Qyw8W8jud";
private static final String pubKey = "010001";
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36";
/**
* 采用网易云音乐加密方式加密
* <p>encText: AES.encode(AES.encode(text,nonce),secKey[随机字符串])</p>
* <p>encText就是将text进行两次AES加密模式为CBC 而key第一次是固定值第二次是随机字符串</p>
* <p>encSecKey: RSA.encode(secKey,pubKey,modulus)</p>
* <P>encSecKey的加密方法在网易云的js代码中看不出来 这里是算法是在网络上搜索得知的</P>
* @param json 要加密的json对象 这里使用fastjson会有问题不要使用fastjson2不然加密会失效(fastjson也会偶尔失效多访问几次就好了 不知道原因...
*/
public static String getBody(JSONObject json) throws Exception {
String text = json.toString();
String secKey = randomString();
String encText = AesEncryptor.aesEncrypt(AesEncryptor.aesEncrypt(text, nonce), secKey);
String encSecKey = RsaEncryptor.rsaEncrypt(secKey, pubKey, modulus);
return "params=" + encText + "&encSecKey=" + encSecKey;
}
}
AES加密
/**
* AES加密
*
* @author 白
*/
public class AesEncryptor {
public static String aesEncrypt(String text, String secKey) throws Exception {
byte[] secKeyBytes = secKey.getBytes(StandardCharsets.UTF_8);
SecretKeySpec secretKeySpec = new SecretKeySpec(secKeyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivParameterSpec = new IvParameterSpec("0102030405060708".getBytes());
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] encryptedBytes = cipher.doFinal(text.getBytes());
return Base64.getEncoder().encodeToString(encryptedBytes);
}
}
RSA加密
public class RsaEncryptor {
/**
* 使用RSA公钥加密文本。
*
* @param text 要加密的文本
* @param pubKey 公钥部分,以十六进制表示
* @param modulus 模数,以十六进制表示
* @return 加密后的结果,以十六进制字符串形式返回
*/
public static String rsaEncrypt(String text, String pubKey, String modulus) {
// 反转字符串
String reversedText = new StringBuilder(text).reverse().toString();
// 将字符串转换为十六进制表示的大数
BigInteger textBigInt = new BigInteger(byteArrayToHex(reversedText.getBytes(StandardCharsets.UTF_8)), 16);
BigInteger publicKey = new BigInteger(pubKey, 16);
BigInteger mod = new BigInteger(modulus, 16);
// 进行RSA加密
BigInteger encrypted = textBigInt.modPow(publicKey, mod);
// 将结果转换为十六进制字符串,并确保长度为256个字符
return String.format("%0256x", encrypted);
}
/**
* 将字节数组转换为十六进制字符串。
*
* @param bytes 字节数组
* @return 十六进制字符串
*/
private static String byteArrayToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}
最后写了一段代码进行测试 测试结果没问题后也是终于解决了加密难题
剩下的只需要抓接口发请求就行了