一、遇到的问题
在使用Java程序访问一个PDF文件的URL时,发现无法下载该PDF文件,加上各种header也不行,然后看程序的response.getStatusLine(),发现是一个以前从未见到过的错误:HTTP/1.1 521 。
乍一看是500系列的,对面的服务崩了?但是复制url到浏览器地址栏,发现还是可以正常浏览这个PDF的。
于是清除浏览器缓存,重新访问这个URL,用大鲨鱼(wireshark)监控数据包发现这个URL刷新一次,但是被请求了两次,都被大鲨鱼监控到了,第一次的状态是521,第二次才是200。
所以弄懂第一次请时返回的数据是什么,第二次请求时浏览器的请求头以及参数是什么,然后用程序模拟浏览器的行为,就可以用代码获得数据了。
大鲨鱼监控的截图如下
二、分析请求响应报文
下图是第一次请求以及回应的HTTP Stream,红色部分是请求内容,蓝紫色(就叫蓝色吧)部分是响应内容。
请求没有什么特别的,简单的get方法,请求头也没什么陌生的。
但是响应内容就有意思的多了。这个响应分两部分,一个是响应头,发现Set-Cookie,一个是响应体,从script标签开始到一大串…结束,有用的部分自然是script标签里面的东西。
复制这个内容到IDEA,格式化之后发现了非常反人类的代码,图片如下:
可以看出这个是定义了三个变量x,y,z还有一个函数 f然后循环。。。然后。。。总之很头疼。不过谷歌浏览器可以帮助我们查看这段脚本到底是干什么的。
创建一个html文件,把第一次请求时回应的内容复制到文件里即可。把eval函数换成console.log,用浏览器打开html文件即可查看脚本输出。
输出结果如下:
发现又是一段js代码,如法炮制,再一次用idea格式化这个新的脚本代码。结果如下:
发现这段代码的大意是设置一个名称为__jsl_clearance的cookie,它的值是:
1543922503.114|0|+{一段代码的结果}+;Expires=Tue, 04-Dec-18 12:21:43 GMT;Path=/;
此时先不着急看这段代码的输出结果,先看大鲨鱼(wireshark)监控到的第二次请求情况:
这此请求很容易理解,请求时有cookie,响应的结果也确实是pdf文件,长度为61805 字节,通过浏览器下载PDF文件,查看文件属性也确实是61805字节。
所以如果能够获得完整的cookie,那么通过get方法请求pdf的URL,即可用代码下载这个PDF文件。
这次请求的cookie有两个键值对,一个是__jsluid,它的值通过第一次请求时的响应头: set-cookie获取,另一个是__jsl_clearance,它只需运行那段神秘的脚本获得对应的值,再加上前缀:1543922503.114|0|即可。
大鲨鱼已经给出那段脚本运行的结果:9NisYvdLKC%2BNMZgGcD08dy2YGFU%3D。
所以完整的cookie为:
__jsluid=923e768304c90e8bd92e2113f9163ca6; __jsl_clearance=1543922503.114|0|9NisYvdLKC%2BNMZgGcD08dy2YGFU%3D
此时再用console.log的方法查看这段脚本代码生成的结果,直接提取那段生成字符串:9NisYvdLKC%2BNMZgGcD08dy2YGFU%3D的脚本代码,看输出即可:
此段脚本代码的输出与大鲨鱼捕获的内容一模一样。
至此全套的访问流程已经梳理清楚。
总结一下:
通过wireshark抓包工具监控浏览器请求URL时的数据报文,发现一次刷新请求,实际上确是请求了两次,分析两次请求时的请求头、响应头、响应体即可弄清浏览器是如何请求到数据的。
浏览器在第一次请求时,会收到响应头:set-cookie,获得关键cookie中的一个:__jsluid。
通过运行第一次请求时得到的脚本(毕竟是一个用script标签修饰的脚本,浏览器会自动运行),该脚本生成新的脚本代码,新代码为浏览器提供另一个关键cookie:__jsl_clearance,以及控制浏览器带着这两个cookie再次访问该URL,就可以获取真实的PDF文件。
三、代码实现
用到的依赖包:
<dependency>
<groupId>com.eclipsesource.j2v8</groupId>
<artifactId>j2v8_win32_x86_64</artifactId>
<version>4.6.0</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
具体代码如下:
public class CrackJavaScript {
private static final Logger lg = Logger.getLogger(CrackJavaScript.class);
/**
* 传入pdf文件的URL, 返回下载好的PDF文件
*
* @param pdfUrl pdf文件的URL地址
* @return pdf文件对象
*/
public static File getPDFFile(String pdfUrl) {
String fileName = Utils.MD5(pdfUrl);
File file = new File("pdfFile/" + fileName + ".pdf");
try {
HttpClient client = HttpClients.createDefault();
HttpGet get = new HttpGet(pdfUrl);
setHeader(get);
HttpResponse response = client.execute(get);
String __jsluid = getJsluid(response);
String body = getResponseBodyAsString(response);
String __jsl_clearance = getJslClearance(body);
get = new HttpGet(pdfUrl);
get.setHeader("cookie", __jsluid + "; " + __jsl_clearance);
setHeader(get);
response = client.execute(get);
output(response, file);
} catch (Exception e) {
lg.error(e.getMessage(), e);
}
return file;
}
/**
* 给HttpGet设置一些必要的header
*
* @param get 通过get方法访问pdf资源
*/
private static void setHeader(HttpGet get) {
get.setHeader("Upgrade-Insecure-Requests", "1");
get.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36");
}
/**
* 将HttpResponse输出到文件, 即将pdf输入流写到硬盘.
*
* @param response http响应
* @param file 落地文件
* @throws IOException IO异常
*/
private static void output(HttpResponse response, File file) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(getResponseBodyAsBytes(response));
fileOutputStream.flush();
fileOutputStream.close();
}
/**
* 通过破解动态JavaScript脚本,
* 获取cookie名为 __jsl_clearance的值
*
* @param body 相应内容(一般为第一次请求获取到的动态js字符串)
* @return cookie名为 __jsl_clearance的值
*/
private static String getJslClearance(String body) {
//V8:谷歌开源的运行JavaScript脚本的库. 参数:globalAlias=window, 表示window为全局别名,
// 告诉V8在运行JavaScript代码时, 不要从代码里找window的定义.
V8 runtime = V8.createV8Runtime("window");
//将第一次请求pdf资源时获取到的字符串提取成V8可执行的JavaScript代码
body = body.trim()
.replace("<script>", "")
.replace("</script>", "")
.replace("eval(y.replace(/\\b\\w+\\b/g, function(y){return x[f(y,z)-1]||(\"_\"+y)}))",
"y.replace(/\\b\\w+\\b/g, function(y){return x[f(y,z)-1]||(\"_\"+y)})");
//用V8执行该段代码获取新的动态JavaScript脚本
String result = runtime.executeStringScript(body);
//获取 jsl_clearance 的第一段, 格式形如: 1543915851.312|0|
String startStr = "document.cookie='";
int i1 = result.indexOf(startStr) + startStr.length();
int i2 = result.indexOf("|0|");
String cookie1 = result.substring(i1, i2 + 3);
/*
获取 jsl_clearance 的第二段,格式形如: DW2jqgJO5Bo45yYRKLlFbnqQuD0%3D。
主要原理是: 新的动态JavaScript脚本是为浏览器设置cookie, 且cookie名为__jsl_clearance
其中第一段值(格式形如:1543915851.312|0|)已经明文写好, 用字符串处理方法即可获取.
第二段则是一段JavaScript函数, 需要有V8运行返回,
该函数代码需要通过一些字符串定位, 提取出来, 交给V8运行.
*/
startStr = "|0|'+(function(){";
int i3 = result.indexOf(startStr) + startStr.length();
int i4 = result.indexOf("})()+';Expires");
String code = result.substring(i3, i4).replace(";return", ";");
String cookie2 = runtime.executeStringScript(code);
/*
拼接两段字符串, 返回jsl_clearance的完整的值.
格式形如: 1543915851.312|0|DW2jqgJO5Bo45yYRKLlFbnqQuD0%3D
*/
return cookie1 + cookie2;
}
/**
* 将HTTP响应体转换为字符串返回
*
* @param response HTTP响应
* @return 响应体的字符串形式
* @throws IOException IO异常
*/
private static String getResponseBodyAsString(HttpResponse response) throws IOException {
return IOUtils.readStreamAsString(response.getEntity().getContent(), "UTF-8");
}
/**
* 将HTTP响应体转换为byte数组返回
*
* @param response HTTP响应
* @return 响应体的byte数组形式
* @throws IOException IO异常
*/
private static byte[] getResponseBodyAsBytes(HttpResponse response) throws IOException {
return IOUtils.readStreamAsByteArray(response.getEntity().getContent());
}
/**
* 通过响应头的set-cookie
* 获取cookie名称为__jsluid的值
* @param response HttpResponse
* @return __jsluid的值
*/
private static String getJsluid(HttpResponse response) {
Header header = response.getFirstHeader("set-cookie");
String[] split = header.getValue().split(";");
for (String s : split) {
if (s.contains("__jsluid")) {
return s.trim();
}
}
return "";
}
}
参考文章:
[1]: http://blog.51cto.com/12925223/2309945
[2]: https://github.com/jhao104/memory-notes/blob/master/Python/Python爬虫—破解JS加密的Cookie.md