Instagram 模拟登录

需求

提供用户名|密码,模拟登录

分析 http 请求

打开浏览器,访问 https://www.instagram.com/ , 随便输入用户名密码,点击登录后,可以在控制台看到如下请求

微信截图_20200609204756.png

一般模拟请求都会关注 请求头,请求体,也就上面的RequestDataFormData,大概的看一下,可以知道一些需要解决的地方有:

  1. header中的x-csrftoken、x-ig-app-id以及x-instagram-ajax从哪里获取?(实际上在后面的测试过程中,我们仅需要csrftoken即可,所以这里只获取csrftoken即可)
  2. FormData中的enc_password是加过密的,加密的逻辑又是什么

token的获取

token的获取一般有两种方式,一种是查看页面源代码,可以看到csrftoken就在里面:

微信截图_20200609220259.png

另一种,直接访问 https://www.instagram.com/data/shared_data/ , 返回:

微信图片_20200609221350.png

不仅包含了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);
    }
}

二次认证

如果开启了登录二次认证,那么在用户名密码验证通过之后,会返回如下信息:

微信截图_20200617003225.png

此时请求的响应码不是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 这里查看

Jackson 添加额外的属性
java读取win10 chrome(80+) cookie