最近需要爬取微博的一些数据,发现某些请求需要有登陆状态才会有正确的响应结果
网上查了下,教程都比较老了,这里写下最新的登陆过程
Http 请求工具
常用的就是 HttpClient,okHttp,我发现依赖很多,而且使用也很麻烦
于是就上 GitHub 上找,终于找到了这个:hsiafan/requests
如果不用 Json 功能,可以说是 0 依赖了。作者是仿照 Python 的 request 库设计的,所以使用起来很简单,而且还支持链式调用。同时对 Https,自定义请求,文件上传,代理,都是支持的,真的是强烈推荐。
模拟登陆
分为两步
-
预登陆
-
登陆
地址如下
public static final String URL_PRE_LOGIN = "https://login.sina.com.cn/sso/prelogin.php";
public static final String URL_LOGIN = "https://login.sina.com.cn/sso/login.php";
预登陆
主要是获取 pubkey,nonce 和 servertime这些参数,然后对后继的登陆的密码做加密
/**
* 预登陆
*
* @return
* @throws Exception
*/
public Map<String, String> preLogin() throws Exception {
HashMap<String, String> params = new HashMap<String, String>();
params.put("entry", "weibo");
// jsonp的函数名
params.put("callback", "sinaSSOController.preloginCallBack");
params.put("rsakt", "mod");
params.put("checkpin", "1");
params.put("client", "ssologin.js(v1.4.19)");
params.put("_", String.valueOf(System.currentTimeMillis()));
// 对用户名先URLEncoder编码再进行Base64编码
params.put("su", this.encodeUserName(this.getUserName()));
RequestBuilder request = Requests.get(URL_PRE_LOGIN);
request.params(params);
RawResponse resp = request.send();
String respBody = resp.readToText();
// 去除最层jsonp函数名,得到json串
respBody = respBody.substring(params.get("callback").length() + 1, respBody.length() - 1);
Map<String, String> result = JSONObject.parseObject(respBody, new TypeReference<Map<String, String>>() {
});
if (!"0".equals(result.get("retcode"))) {
throw new Exception("预登陆失败:" + JsonKit.toJson(result));
}
return result;
}
获取的数据如下
sinaSSOController.preloginCallBack(json字符串)
json 的内容
登陆
与登陆成功后拿到一些关键信息再进行登陆,最主要的是对密码的加密
/**
* 对密码进行加密
* RSA算法原因,每次结果不一样,是正常的,同样的参数和抓包的不一样是正常的
* 加密过程可以看https://login.sina.com.cn/signup/signin.php中的ssologin.js
*
* @param servertime
* @param nonce
* @param password
* @param pubKey
* @return
* @throws Exception
*/
public String encodePwd(String servertime, String nonce, String password, String pubKey) throws Exception {
// ssologin.js 822行
String toEncode = servertime + "\t" + nonce + "\n" + password;
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
BigInteger modulus = new BigInteger(pubKey, 16);
BigInteger publicExponent = new BigInteger("10001", 16);
RSAPublicKeySpec rsaPublicKeySpec = new RSAPublicKeySpec(modulus, publicExponent);
PublicKey publicKey = keyFactory.generatePublic(rsaPublicKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encodeStr = cipher.doFinal(toEncode.getBytes());
return HashKit.toHex(encodeStr);
}
登陆代码
/**
* 登陆
* @param body,预登陆得到的json
* @return
* @throws Exception
*/
public Map<?, ?> login(Map<String, String> body) throws Exception {
// Url parm
body.put("client", "ssologin.js(v1.4.11)");
body.put("_", String.valueOf(System.currentTimeMillis()));
// Form body
body.put("entry", "weibo");
body.put("gateway", "1");
body.put("from", "");
body.put("savestate", "7");
body.put("qrcode_flag", "false");
body.put("useticket", "1");
body.put("pagerefer", "");
body.put("vsnf", "1");
body.put("service", "miniblog");
body.put("pwencode", "rsa2");
body.put("sr", "1366*768");
body.put("domain", "weibo.com");
body.put("cdult", "2");
// 设为TEXT会返回json格式数据
body.put("returntype", "TEXT");
// 对用户名先URLEncoder编码再进行Base64编码
body.put("su", this.encodeUserName(this.getUserName()));
// 密码加密
body.put("sp", this.encodePwd(body.get("servertime"), body.get("nonce"), this.getPassword(), body.get("pubkey")));
body.put("encoding", "UTF-8");
// 100-1000的一个随机数
body.put("prelt", String.valueOf(new Random().nextInt(900) + 100));
RequestBuilder request = Requests.post(URL_LOGIN);
request.body(body);
RawResponse resp = request.send();
// 收集Cookie,很重要!!!!
this.addCookies(resp.getCookies());
Map<?, ?> result = JSONObject.parseObject(resp.readToText(), Map.class);
if (!"0".equals(result.get("retcode").toString())) {
throw new Exception("登陆失败:" + result.get("reason").toString());
}
return result;
}
登陆成功后会返回一个 json 串,会拿到昵称,和三个单点登陆的地址,这些都不管,到这一步就登陆成功了
以后每次请求都带上此次响应的 cookie 就行了,有效期为 21h
一些坑
密码加密
密码加密的算法每次得到的结果是不同的,所以和抓包结果对比老是不一样,一直以为是自己写错了,其实写的是没问题的,和 JS 里面的逻辑是一样的
Cookie
requests 请求工具有会话管理功能 Session 类,即维护 Cookie,每次请求会自动带上之前的 Cookie。关键是它的 Cookie 是区分域名的,登陆是 login.sina.com.cn,微博页面是 weibo.com,所以登陆页面的 Cookie 是不会带到 weibo.com。所以它的这个 Session 类不适合这个场景。
我的做法是把 login.sina.com.cn 的 Cookie 转成字符串给 weibo.com 用
StringBuilder sb = new StringBuilder();
for (String name : this.cookies.keySet()) {
sb.append(name).append("=").append(this.cookies.get(name));
sb.append("; ");
}
return sb.substring(0, sb.length() - 1);
// 直接放到请求头
headers.put("Cookie", this.getCookiesStr());
验证码
这个实在是解决不了,在本地是没问题的,部署到服务器上相当于你的微博账号异地登陆了,要输入验证码
解决办法是本地登陆获得 cookie,放到服务器,缺点是需要每天更新。
这样的话上面写了一大堆模拟登陆就没啥卵用了,直接浏览器登陆,F12 看下 Cookie 复制不就行了,但是目前的能力是只能做到这一步
验证码的请求地址如下,三个参数含义未知,也懒得搞了,拿到验证码还要图像识别,这个更做不了
https://login.sina.com.cn/cgi/pin.php?r=35782237&s=0&p=gz-3336bddc64b3a1f3d3d86f751394fd43c3be