TMaize Blog

微博模拟登陆

最近需要爬取微博的一些数据,发现某些请求需要有登陆状态才会有正确的响应结果

网上查了下,教程都比较老了,这里写下最新的登陆过程

Http 请求工具

常用的就是 HttpClient,okHttp,我发现依赖很多,而且使用也很麻烦

于是就上 GitHub 上找,终于找到了这个:hsiafan/requests

如果不用 Json 功能,可以说是 0 依赖了。作者是仿照 Python 的 request 库设计的,所以使用起来很简单,而且还支持链式调用。同时对 Https,自定义请求,文件上传,代理,都是支持的,真的是强烈推荐。

模拟登陆

分为两步

  1. 预登陆

  2. 登陆

地址如下

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 的内容

sina-01

登陆

与登陆成功后拿到一些关键信息再进行登陆,最主要的是对密码的加密

/**
 * 对密码进行加密
 * 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

sina-02

一些坑

密码加密

密码加密的算法每次得到的结果是不同的,所以和抓包结果对比老是不一样,一直以为是自己写错了,其实写的是没问题的,和 JS 里面的逻辑是一样的

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