PHP抓取新版正方教務系統獲取課程表(及RSA加密密碼實現)
前言
相比舊版的教務系統,唯一好處是不用輸入驗證碼方便爬蟲登錄。但登錄時用到RSA加密密碼發送請求。
登錄請求分析
在登錄頁面上填上隨便寫的賬號密碼,點擊登錄,瀏覽器開發者工具網絡請求如下:
首先它點擊登錄後,提交一個表單,Form Data一共有4個數據
提交的數據 | 解釋 |
---|---|
csrftoken | 爲了防止跨站域請求僞造 。在登錄頁源碼裏有,每次刷新都會變更 |
yhm | 輸入的用戶名(學號) |
mm | 輸入的密碼,被加密過。我們主要關注這一個加密過程 |
csrftoken獲取
解析登錄頁面的表單標籤,每次都不一樣,取出 input[name=csrftoken] 的值即可。
RSA公鑰獲取
在開發者工具的網絡請求列表,可以看到這個請求鏈接,發送當前時間戳,並且每次modulus的數據不同,用該數據對登錄密碼進行加密,如何加密就是後面的重點要敘述的內容了。
原網站的js分析
$.getJSON(_path+"/xtgl/login_getPublicKey.html?time="+new Date().getTime(),function(data){
modulus = data["modulus"];
exponent = data["exponent"];
});
if($("#mmsfjm").val() == '0'){
$("#hidMm").val($("#mm").val());
}else{
var rsaKey = new RSAKey();
rsaKey.setPublic(b64tohex(modulus), b64tohex(exponent));
var enPassword = hex2b64(rsaKey.encrypt($("#mm").val()));
$("#mm").val(enPassword);
$("#hidMm").val(enPassword);
}
加密過程
首先將modulus,exponent轉base64,然後再轉16進制,再通過RSA算法生成公鑰,
用公鑰將密碼生成密文,最後從16進制轉回base64的加密字符串。
至此,我們分析完成,在PHP裏面要做的步驟:
1、獲取到csrftoken
2、發送時間戳獲取到PublicKey
3、生成RSA加密的密碼
4、POST請求登錄
密碼RSA加密方式
第一種方式:可以在前端通過js處理加密,然後傳給後端
這種方式其實就是把正方教務系統登錄頁加密相關的js下載到自己項目中,用官網一樣方式來處理加密,這種方式可靠性高,接入成本低,易實現,有個缺點是如果我們的應用有綁定功能就顯得有點麻煩。
具體實現請看這篇博文:
第二種方式:後端代碼(PHP/JAVA)執行js
在PHP/Java中直接運行JS文件,簡單的JS還可以,如果有的JS文件中會有navigator、window,javax.script.ScriptEngine是無法解析的。
第三種方式:後端通過標準的RSA加密處理
看起來很順暢的思路,但是遇到很多坑,主要是在對密碼加密的時候,PHP與JavaScript在對數據進行RSA加密有些區別,由於正方在RSA加密錢和加密後有對數據有自己的預處理邏輯。
JavaScript在加密前對數據進行了隨機填充,並用RSA/None/NoPadding的填充方式來加密,每一次得到的每一次結果都不同;JPHP在RSA加密時默認的填充方式爲OPENSSL_PKCS1_PADDING。我選擇OPENSSL_NO_PADDING的填充方式初始化公鑰並沒成功。
第四種方式:後端代碼JavaScript前端加密方式實現
這種方式應該算是一種優雅的方式,但是按js的代碼邏輯來寫一遍後端邏輯,成本是非常大的,可靠性並比一定高,這裏還是有牛人已經實現了,這裏分享出來。
Java將JavaScript前端加密方式實現
public class ConnectJWGL {
private final String url = "http://www.zfjw.xupt.edu.cn";
private Map<String,String> cookies = new HashMap<>();
private String modulus;
private String exponent;
private String csrftoken;
private Connection connection;
private Connection.Response response;
private Document document;
private String stuNum;
private String password;
public ConnectJWGL(String stuNum,String password){
this.stuNum = stuNum;
this.password = password;
}
public void init() throws Exception{
getCsrftoken();
getRSApublickey();
beginLogin();
}
// 獲取csrftoken和Cookies
private void getCsrftoken(){
try{
connection = Jsoup.connect(url+ "/jwglxt/xtgl/login_slogin.html?language=zh_CN&_t="+new Date().getTime());
connection.header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0");
response = connection.timeout(5000).execute();
cookies = response.cookies();
document = Jsoup.parse(response.body());
csrftoken = document.getElementById("csrftoken").val();
}catch (Exception ex){
ex.printStackTrace();
}
}
// 獲取公鑰並加密密碼
private void getRSApublickey() throws Exception{
connection = Jsoup.connect(url+ "/jwglxt/xtgl/login_getPublicKey.html?" +
"time="+ new Date().getTime());
connection.header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0");
response = connection.cookies(cookies).ignoreContentType(true).timeout(5000).execute();
JSONObject jsonObject = JSON.parseObject(response.body());
modulus = jsonObject.getString("modulus");
exponent = jsonObject.getString("exponent");
password = RSAEncoder.RSAEncrypt(password, B64.b64tohex(modulus), B64.b64tohex(exponent));
password = B64.hex2b64(password);
}
//登錄
public boolean beginLogin() throws Exception{
connection = Jsoup.connect(url+ "/jwglxt/xtgl/login_slogin.html");
connection.header("Content-Type","application/x-www-form-urlencoded;charset=utf-8");
connection.header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0");
connection.data("csrftoken",csrftoken);
connection.data("yhm",stuNum);
connection.data("mm",password);
connection.data("mm",password);
connection.cookies(cookies).ignoreContentType(true)
.method(Connection.Method.POST).execute();
response = connection.execute();
document = Jsoup.parse(response.body());
if(document.getElementById("tips") == null){
System.out.println("登陸成功");
return true;
}else{
System.out.println(document.getElementById("tips").text());
return false;
}
}
}
RSA加密
public class RSAEncoder {
private static BigInteger n = null;
private static BigInteger e = null;
public static String RSAEncrypt(String pwd, String nStr, String eStr){
n = new BigInteger(nStr,16);
e = new BigInteger(eStr,16);
BigInteger r = RSADoPublic(pkcs1pad2(pwd,(n.bitLength()+7)>>3));
String sp = r.toString(16);
if((sp.length()&1) != 0 )
sp = "0" + sp;
return sp;
}
private static BigInteger RSADoPublic(BigInteger x){
return x.modPow(e, n);
}
private static BigInteger pkcs1pad2(String s, int n){
if(n < s.length() + 11) { // TODO: fix for utf-8
System.err.println("Message too long for RSAEncoder");
return null;
}
byte[] ba = new byte[n];
int i = s.length()-1;
while(i >= 0 && n > 0) {
int c = s.codePointAt(i--);
if(c < 128) { // encode using utf-8
ba[--n] = new Byte(String.valueOf(c));
}
else if((c > 127) && (c < 2048)) {
ba[--n] = new Byte(String.valueOf((c & 63) | 128));
ba[--n] = new Byte(String.valueOf((c >> 6) | 192));
} else {
ba[--n] = new Byte(String.valueOf((c & 63) | 128));
ba[--n] = new Byte(String.valueOf(((c >> 6) & 63) | 128));
ba[--n] = new Byte(String.valueOf((c >> 12) | 224));
}
}
ba[--n] = new Byte("0");
byte[] temp = new byte[1];
Random rdm = new Random(47L);
while(n > 2) { // random non-zero pad
temp[0] = new Byte("0");
while(temp[0] == 0)
rdm.nextBytes(temp);
ba[--n] = temp[0];
}
ba[--n] = 2;
ba[--n] = 0;
return new BigInteger(ba);
}
}
Base64與十六進制的相互轉化
public class B64 {
public static String b64map="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
private static char b64pad = '=';
private static String hexCode = "0123456789abcdef";
// 獲取對應16進制字符
public static char int2char(int a){
return hexCode.charAt(a);
}
// Base64轉16進制
public static String b64tohex(String s) {
String ret = "";
int k = 0;
int slop = 0;
for(int i = 0; i < s.length(); ++i) {
if(s.charAt(i) == b64pad) break;
int v = b64map.indexOf(s.charAt(i));
if(v < 0) continue;
if(k == 0) {
ret += int2char(v >> 2);
slop = v & 3;
k = 1;
}
else if(k == 1) {
ret += int2char((slop << 2) | (v >> 4));
slop = v & 0xf;
k = 2;
}
else if(k == 2) {
ret += int2char(slop);
ret += int2char(v >> 2);
slop = v & 3;
k = 3;
}
else {
ret += int2char((slop << 2) | (v >> 4));
ret += int2char(v & 0xf);
k = 0;
}
}
if(k == 1)
ret += int2char(slop << 2);
return ret;
}
// 16進制轉Base64
public static String hex2b64(String h) {
int i , c;
StringBuilder ret = new StringBuilder();
for(i = 0; i+3 <= h.length(); i+=3) {
c = parseInt(h.substring(i,i+3),16);
ret.append(b64map.charAt(c >> 6));
ret.append(b64map.charAt(c & 63));
}
if(i+1 == h.length()) {
c = parseInt(h.substring(i,i+1),16);
ret.append(b64map.charAt(c << 2));
}
else if(i+2 == h.length()) {
c = parseInt(h.substring(i,i+2),16);
ret.append(b64map.charAt(c >> 2));
ret.append(b64map.charAt((c & 3) << 4));
}
while((ret.length() & 3) > 0) ret.append(b64pad);
return ret.toString();
}
}
完整的源碼:Semi-automatic-Crawl-JWGL
PHP實現方式博主正在測試中: