微博模擬登陸
最近需要爬取微博的一些資料,發現某些請求需要有登陸狀態才會有正確的響應結果
網上查了下,教程都比較老了,這裡寫下最新的登陸過程
Http 請求工具
常用的就是 HttpClient,okHttp,我發現依賴很多,而且使用也很麻煩
於是就上 GitHub 上找,終於找到了這個: ofollow,noindex" target="_blank">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