需求
提供用户名|密码,模拟登录
分析 http 请求
打开浏览器,访问 https://www.instagram.com/ , 随便输入用户名密码,点击登录后,可以在控制台看到如下请求
一般模拟请求都会关注 请求头,请求体,也就上面的RequestData
和FormData
,大概的看一下,可以知道一些需要解决的地方有:
- header中的x-csrftoken、x-ig-app-id以及x-instagram-ajax从哪里获取?(实际上在后面的测试过程中,我们仅需要csrftoken即可,所以这里只获取csrftoken即可)
FormData
中的enc_password
是加过密的,加密的逻辑又是什么
token的获取
token的获取一般有两种方式,一种是查看页面源代码,可以看到csrftoken就在里面:
另一种,直接访问 https://www.instagram.com/data/shared_data/ , 返回:
不仅包含了token,还包含了用于加密的参数,这对接下来的密码加密至关重要
密码加密实现
实现的算法比较复杂,具体的翻译可以看:https://pastebin.com/raw/nYL2W2bG ,其中一些参数比较难懂,感兴趣的可以看看原始实现:
https://www.instagram.com/static/bundles/es6/DistilleryPasswordForm.js/b7a7993aaf9e.js ,这里面最让人疑惑的是这个方法:tweetnacl.sealedbox.seal
,还好,java版本也有对应的实现:https://stackoverflow.com/questions/42456624/how-can-i-create-or-open-a-libsodium-compatible-sealed-box-in-pure-java
最后附上java版本的实现:
int key = 64;
String pkey = "555026eac0a4d140916813b6e0fa18acf72fde978f212ffd61207def77e26065";
int overheadLength = 48;
byte[] pkeyArray = new byte[pkey.length() / 2];
for (int i = 0; i < pkeyArray.length; i++) {
int index = i * 2;
int j = Integer.parseInt(pkey.substring(index, index + 2), 16);
pkeyArray[i] = (byte) j;
}
byte [] y = new byte[password.length()+36+16+overheadLength];
int f = 0;
y[f] = 1;
y[f += 1] = (byte)key;
f += 1;
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(256);
// Generate Key
SecretKey secretKey = keyGenerator.generateKey();
byte[] IV = new byte[12];
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, IV);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec);
cipher.updateAAD(time.getBytes());
byte [] sealed = SealedBoxUtility.crypto_box_seal(secretKey.getEncoded(),pkeyArray);
byte[] cipherText = cipher.doFinal(password.getBytes());
y[f] = (byte) (255 & sealed.length);
y[f + 1] = (byte) (sealed.length >> 8 & 255);
f += 2;
for(int j=f;j<f+sealed.length;j++){
y[j] = sealed[j-f];
}
f += 32;
f += overheadLength;
byte [] c = Arrays.copyOfRange(cipherText,cipherText.length -16,cipherText.length);
byte [] h = Arrays.copyOfRange(cipherText,0,cipherText.length - 16);
for(int j=f;j<f+c.length;j++){
y[j] = c[j-f];
}
f += 16;
for(int j=f;j<f+h.length;j++){
y[j] = h[j-f];
}
return Base64.getEncoder().encodeToString(y);
发送请求
private String login(String username,String password) throws Exception {
CloseableHttpClient client = Https.newHttpClient();
HttpClientContext context = new HttpClientContext();
Https.connect(client,new HttpGet(URL_PREFIX),context);
String csrf = context.getCookieStore().getCookies().stream().filter(c->CSRF_TOKEN_COOKIE_NAME.equals(c.getName())).map(Cookie::getValue).findAny().orElseThrow();
HttpPost post = new HttpPost("https://www.instagram.com/accounts/login/ajax/");
String time = String.valueOf(System.currentTimeMillis()/1000);
post.addHeader("X-CSRFToken",csrf);
List<NameValuePair> pairs = new ArrayList<>();
pairs.add(new BasicNameValuePair("username",username));
pairs.add(new BasicNameValuePair("enc_password","#PWD_INSTAGRAM_BROWSER:10:"+time+":"+encrypt(password,time)));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(pairs);
post.setEntity(entity);
String content = Https.toString(client,post,context);
if(content.contains("\"authenticated\": true")) {
return context.getCookieStore().getCookies().stream().filter(c->SESSION_ID_COOKIE_NAME.equals(c.getName())).map(Cookie::getValue).findAny().orElseThrow();
} else {
throw new LogicException("登录失败:" + content);
}
}
二次认证
如果开启了登录二次认证,那么在用户名密码验证通过之后,会返回如下信息:
此时请求的响应码不是200,而是400!
二次认证相对很简单,直接贴代码了:
private static void twoFactorLogin(String username, String content, String csrfToken, CloseableHttpClient client, HttpClientContext context,boolean exit) throws Exception {
Scanner scan = new Scanner(System.in);
System.out.println("请输入二次认证码");
String code = scan.nextLine();
System.out.println("获取二次认证码:"+code+",开始认证");
Utils.ExpressionExecutor ee = Utils.readJson(content);
String identifier = ee.execute("two_factor_info->two_factor_identifier").get();
HttpPost post = new HttpPost("https://www.instagram.com/accounts/login/ajax/two_factor/");
post.addHeader("X-CSRFToken",csrfToken);
List<NameValuePair> pairs = new ArrayList<>();
pairs.add(new BasicNameValuePair("username",username));
pairs.add(new BasicNameValuePair("identifier",identifier));
pairs.add(new BasicNameValuePair("queryParams","{\"next\":\"/\"}"));
pairs.add(new BasicNameValuePair("verificationCode",code));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(pairs);
post.setEntity(entity);
try{
Https.toString(client,post,context);
String sid = context.getCookieStore().getCookies().stream().filter(c->"sessionid".equals(c.getName())).map(Cookie::getValue).findAny().get();
System.out.println("登录成功");
}catch (Https.InvalidStateCodeException e){
Utils.ExpressionExecutor ee2 = Utils.readJson(e.getContent());
String errorType = ee2.execute("error_type").orElse(null);
if("sms_code_validation_code_invalid".equals(errorType)){
twoFactorLogin(username,content,csrfToken,client,context,exit);
} else if("invalid_identifier".equals(errorType)) {
login(client,exit);
} else {
System.out.println("登录失败");
if(exit) {
System.exit(-1);
}
}
}
}
完整的代码可以到 https://github.com/mhlx/downins/blob/master/src/main/java/me/qyh/downinsrun/parser/ParseUtils.java 这里查看