SocialSisterYi / bilibili-API-collect

哔哩哔哩-API收集整理【不断更新中....】
https://socialsisteryi.github.io/bilibili-API-collect/
Other
15.34k stars 1.74k forks source link

安卓端哔哩哔哩漫画密码登录接口始终返回验证码错误 (错误码: -105) #665

Closed Cdm2883 closed 6 months ago

Cdm2883 commented 1 year ago

我该如何正确从服务端获取登录信息?

我的登录过程

  1. 输入用户账户及明文密码
  2. 获取公钥和盐将明文密码加密 (过程与获取公钥&盐(web端)无异)
  3. 请求POST x-www-form-urlencoded https://passport.bilibili.com/x/passport-login/oauth2/login携带ts (当前10位时间戳), username (用户账户)及password (密文密码)最后签名. 这一步服务端返回的内容结构如下:
    {
      "code": -105,
      "message": "验证码错误",
      "ttl": 1,
      "data": {
        "status": 0,
        "message": "",
        "url": "https://www.bilibili.com/h5/project-msg-auth/verify?ct\u003dgeetest\u0026recaptcha_token\u003d【recaptcha_token】\u0026gee_gt\u003d【gee_gt】\u0026gee_challenge\u003d【gee_challenge】\u0026hash\u003d【...】",
        "token_info": null,
        "cookie_info": null,
        "sso": null,
        "is_new": false,
        "is_tourist": false
      }
    }

    通过json_root.data.url的链接截取Geetest (极验)验证码的recaptcha_token, gee_gtgee_challenge

  4. 通过已知内容获取Geetest (极验)验证码的gee_validate
  5. 再次请求https://passport.bilibili.com/x/passport-login/oauth2/login但是附加携带gee_challenge, gee_seccode, gee_validate, recaptcha_token最后签名.

在抓包中, 第五步哔哩哔哩漫画安卓客户端已经正确从服务端获取登录信息, 但是在我的实现过程中服务端仍然返回如第3步的内容即验证码错误.

签名过程

过程与API 签名与鉴权无异, 使用的参数如下 APPKEY APPSEC
cc8617fd6961e070 3131924b941aac971e45189f265262be

appkey通过抓包获取. appsec通过反编译获取, 位于com.bilibili.comic.push.PushHelper$1 d函数处.
经验证, 可以正常使用

Geetest (极验)验证码的gee_validate获取方式

  1. 手动验证器
  2. 访问登录过程第3步json_root.data.url处链接, 抓包ajax.php获取

我的Java代码实现

代码经过处理和脱敏以便可以只用一个文件就能运行
需要Java 14 (因为使用了instanceof的模式, 如需再低版本Java运行可以将第241行取消注释再删除模式), 需要导入库:

implementation 'cn.hutool:hutool-all:5.8.18'
implementation 'com.alibaba:fastjson:1.2.83_noneautotype'
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
代码过长, 请展开查看

```java package vip.cdms; import cn.hutool.core.codec.Base64; import com.alibaba.fastjson.JSONObject; import okhttp3.*; import okio.Buffer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.crypto.Cipher; import java.io.IOException; import java.math.BigInteger; import java.security.KeyFactory; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.spec.X509EncodedKeySpec; import java.util.HashMap; import java.util.Map; import java.util.Scanner; import java.util.TreeMap; public class BiliMangaLoginTest { public static final String appKey = "cc8617fd6961e070"; public static final String appSec = "3131924b941aac971e45189f265262be"; // 别问我怎么搞到的😋 public static class BiliAPIError extends Exception { private final int code; // private final String codeString; private final String message; public BiliAPIError(int code, String message) { this.code = code; // codeString = String.valueOf(code); this.message = message; } public BiliAPIError(String code, String message) { this.code = -1; // codeString = code; this.message = message; } public int getCode() { return code; } // public String getCodeString() { // return codeString; // } @Nullable @Override public String getMessage() { return message; } @NotNull @Override public String toString() { return this.getClass().getName() + ": code " + code + ", message " + message; } } public static class API { public interface JsonDataCallback { void onFailure(Exception e, JSONObject json_root); void onResponse(T json_root_data); } public interface JsonDataCallbackAutoE { void onResponse(T json_root_data); } public static JsonDataCallback getJsonDataCallbackAutoE( JsonDataCallback callback, JsonDataCallbackAutoE jsonDataCallbackAutoE ) { return new JsonDataCallback<>() { @Override public void onFailure(Exception e, JSONObject json_root) { callback.onFailure(e, json_root); } @Override public void onResponse(T json_root_data) { jsonDataCallbackAutoE.onResponse(json_root_data); } }; } public static class OkhttpJsonDataCallback implements Callback { private final JsonDataCallback callback; public OkhttpJsonDataCallback(JsonDataCallback callback) { this.callback = callback; } @Override public void onFailure(@NotNull Call call, @NotNull IOException e) { callback.onFailure(e, null); } @Override public void onResponse(@NotNull Call call, @NotNull Response response) { String result; try { assert response.body() != null; result = response.body().string(); } catch (IOException e) { callback.onFailure(e, null); return; } try { JSONObject json_root = JSONObject.parseObject(result); Object code_ = json_root.get("code"); if (code_ instanceof Number) { int code = (Integer) code_; String msg = json_root.getString("msg"); if (msg == null) msg = json_root.getString("message"); if (code != 0) throw new BiliAPIError(code, msg/* + response.request().url().url().toString()*/); } else if (code_ instanceof String) { String code = (String) code_; String msg = json_root.getString("msg"); if (msg == null) msg = json_root.getString("message"); throw new BiliAPIError(code, msg); } callback.onResponse((T) json_root.get("data")); } catch (Exception e) { JSONObject json_root = null; try { json_root = JSONObject.parseObject(result); } catch (Exception ignored) {} callback.onFailure(e, json_root); } } } } public static class Geetest { public String verify_url; public String recaptcha_token; public String gee_gt; public String gee_challenge; public String hash; public String gee_validate; @Override public String toString() { return super.toString() + "\n" + " | verify_url --> " + verify_url + "\n" + " | recaptcha_token --> " + recaptcha_token + "\n" + " | gee_gt --> " + gee_gt + "\n" + " | gee_challenge --> " + gee_challenge + "\n" + " | hash --> " + hash + "\n" + " | gee_validate --> " + gee_validate; } } public interface LoginCallback extends API.JsonDataCallback { void onFailure(Exception e, JSONObject json_root); void onCaptcha(Geetest geetest); void onResponse(JSONObject json_root_data); } public static void login( String username, String password, @Nullable Geetest geetest, LoginCallback callback ) { OkHttpClient client = new OkHttpClient.Builder().build(); Request keyRequest = new Request.Builder() .url( HttpUrl.get("https://passport.bilibili.com/x/passport-login/web/key") .newBuilder() .addQueryParameter("appkey", appKey) .build() ) .build(); client.newCall(keyRequest).enqueue(new API.OkhttpJsonDataCallback<>(API.getJsonDataCallbackAutoE(callback, key_root_data -> { String hash = key_root_data.getString("hash"); String key = key_root_data.getString("key"); String encodedPassword; try { encodedPassword = encodePassword(hash, key, password); } catch (Exception e) { throw new RuntimeException(e); } FormBody.Builder formBodyBuilder = new FormBody.Builder(); sign(new HashMap<>(){{ if (geetest != null) { put("gee_challenge", geetest.gee_challenge); put("gee_seccode", geetest.gee_validate + "|jordan"); put("gee_validate", geetest.gee_validate); put("recaptcha_token", geetest.recaptcha_token); } put("ts", String.valueOf(System.currentTimeMillis() / 1000)); put("username", username); put("password", encodedPassword); // put("bili_local_id", "b50481149e09bbd4cead72e7346c576920230428174741576eeca49a783a7fe6"); // put("build", "36505100"); // put("buvid", "XY9D69892101BD675DEFC72F94764180A7746"); // put("c_locale", ""); // put("channel", "dedaobook"); // put("device", "phone"); // // put("device_id", "【...】"); // put("device_meta", "【...】"); // put("device_name", "【...】"); // put("device_platform", "【...】"); // put("device_tourist_id", ""); // put("dt", "【...】"); // // put("extend", ""); // put("from_pv", ""); // put("from_url", ""); // put("is_teenager", "0"); // put("local_id", "XY9D69892101BD675DEFC72F94764180A7746"); // put("login_session_id", ""); // put("mobi_app", "android_comic"); // put("no_recommend", "0"); // put("platform", "android"); // put("s_locale", ""); // put("spm_id", ""); }}, formBodyBuilder, appKey, appSec); Request postRequest = new Request.Builder() .url("https://passport.bilibili.com/x/passport-login/oauth2/login") // .addHeader("buvid", "XY9D69892101BD675DEFC72F94764180A7746") // .addHeader("user-agent", "Mozilla/5.0 BiliComic/5.5.1 os/android model/【...】 mobi_app/android_comic build/36505100 channel/dedaobook innerVer/36505100 osVer/10 network/2") // .addHeader("x-bili-trace-id", "【...】") .post(formBodyBuilder.build()) .build(); client.newCall(postRequest).enqueue(new API.OkhttpJsonDataCallback<>(new API.JsonDataCallback() { @Override public void onFailure(Exception e, JSONObject json_root) { e.printStackTrace(); if (!(e instanceof BiliAPIError biliAPIError)) { callback.onFailure(e, json_root); return; } // BiliAPIError biliAPIError = (BiliAPIError) e; if (biliAPIError.getCode() == -105) { Geetest geetest = new Geetest(); String captchaUrl = json_root.getJSONObject("data").getString("url"); geetest.verify_url = captchaUrl; String[] queries = captchaUrl.replace("https://www.bilibili.com/h5/project-msg-auth/verify?", "").split("&"); for (String query : queries) { String[] kv = query.split("="); String key = kv[0]; String value = kv[1]; switch (key) { case "recaptcha_token": geetest.recaptcha_token = value; break; case "gee_gt": geetest.gee_gt = value; break; case "gee_challenge": geetest.gee_challenge = value; case "hash": geetest.hash = value; } } callback.onCaptcha(geetest); } else { callback.onFailure(biliAPIError, json_root); } } @Override public void onResponse(JSONObject json_root_data) { callback.onResponse(json_root_data); } })); }))); } private static String encodePassword(String salt, String key, String password) throws Exception { String[] keySplit = key.strip().split("\n"); String keyContent = keySplit[1] + keySplit[2] + keySplit[3] + keySplit[4]; KeyFactory keyFactory = KeyFactory.getInstance("RSA"); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.decode(keyContent)); PublicKey publicKey = keyFactory.generatePublic(keySpec); // Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.PUBLIC_KEY, publicKey); byte[] bytes = cipher.doFinal((salt + password).getBytes()); return /*new String(*/Base64.encode(bytes)/*.getBytes(), "ISO_8859_1")*/; } public static void sign(Map map, @Nullable FormBody.Builder out, String appKey, String appSec) { // 按照key重新排序 Map sortedMap = new TreeMap<>(String::compareTo); sortedMap.put("appkey", appKey); for (String key : map.keySet()) sortedMap.put(key, map.get(key)); // 构造url query FormBody.Builder formBuilder = new FormBody.Builder(); for (String key : sortedMap.keySet()) { String value = sortedMap.get(key); if (value == null) continue; formBuilder.add(key, value); if (out != null) out.add(key, value); } String urlQuery; try (Buffer buffer = new Buffer()) { formBuilder.build().writeTo(buffer); urlQuery = buffer.readUtf8(); } catch (IOException e) { return; } // 计算hash StringBuilder sign = new StringBuilder(urlQuery + appSec); try { MessageDigest md5 = MessageDigest.getInstance("MD5"); md5.update(sign.toString().getBytes()); sign = new StringBuilder(new BigInteger(1, md5.digest()).toString(16)); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } while (sign.length() != 32) sign.insert(0, "0"); if (out != null) out.add("sign", sign.toString()); } private static final Scanner scanner = new Scanner(System.in); private static void login(String username, String password, Geetest geetest, API.JsonDataCallback callback) { login(username, password, geetest, new LoginCallback() { @Override public void onFailure(Exception e, JSONObject json_root) { callback.onFailure(e, json_root); } @Override public void onCaptcha(Geetest geetest) { System.out.println(); System.out.println(geetest.toString()); System.out.println("pls input 'gee_validate': "); geetest.gee_validate = scanner.nextLine(); if (geetest.gee_validate.equals("stop")) System.exit(0); login(username, password, geetest, callback); } @Override public void onResponse(JSONObject json_root_data) { callback.onResponse(json_root_data); } }); } public static void main(String[] args) { System.out.println("pls input 'username': "); String username = scanner.nextLine(); System.out.println("pls input 'password': "); String password = scanner.nextLine(); Geetest geetest = new Geetest(); login(username, password, geetest, new API.JsonDataCallback<>() { @Override public void onFailure(Exception e, JSONObject json_root) { e.printStackTrace(); System.exit(0); } @Override public void onResponse(JSONObject json_root_data) { System.out.println(); System.out.println("onResponse"); System.out.println(json_root_data.toJSONString()); System.exit(0); } }); } } ```

FishZe commented 1 year ago

同样的问题,在使用验证码登录那里,返回-3 API校验密匙错误

Cdm2883 commented 1 year ago

同样的问题,在使用验证码登录那里,返回-3 API校验密匙错误

你的问题可能与本问题不相关. -3 API校验密匙错误这个报错应该是因为你的签名过程错误, 或者你没有进行签名

FishZe commented 1 year ago

我的签名应该是没有问题的,使用相同的appkeyappsec,相同的算法计算sign,其他接口都没有问题,只有这个接口返回-3 API校验密匙错误,因此我怀疑是否为这个问题

Cdm2883 commented 1 year ago

我的签名应该是没有问题的,使用相同的appkeyappsec,相同的算法计算sign,其他接口都没有问题,只有这个接口返回-3 API校验密匙错误,因此我怀疑是否为这个问题

可能是你所使用的接口只有特定客户端可使用, 需使用特定客户端的appkeyappsec. 经检查我的签名过程没有错误可以正常进行签名, 刚才已尝试使用其他客户端的appkeyappsec进行登录操作, 仍然会报错-105 验证码错误

Sualiu commented 11 months ago

发现原因了嘛?emm……遇到一毛一样的问题哩。

Sualiu commented 11 months ago

发现问题了,极验的手动登录验证太老旧了,换成新版本,把challenge参数更新一下。

JiunnTarn commented 11 months ago

发现问题了,极验的手动登录验证太老旧了,换成新版本,把 challenge 参数更新一下。

大佬,请问是 手动验证器 太老旧了吗?我使用 极验官方的 Android SDK也遇到这个问题,拿到 validate 后登录,仍然返回 -105 验证码错误

Sualiu commented 11 months ago

极验验证成功后会返回三个值: geetest_challenge; geetest_validate; geetest_seccode;

而手动验证器只返回了geetest_validate,geetest_seccode倆值,少个geetest_challenge(这个challenge和最开始的不一样),所以传给哔哩哔哩challenge值也就不对了。

Sualiu commented 11 months ago

手动验证器 的代码改一下就行,在 js代码的

validate.value = result.geetest_validate;
seccode.value = result.geetest_seccode;

后面加个 challenge.value = result.geetest_challenge;

把这个值也获取一下就行。

JiunnTarn commented 11 months ago

极验验证成功后会返回三个值: geetest_challenge; geetest_validate; geetest_seccode; 而手动验证器只返回了 geetest_validate,geetest_seccode 倆值,少个 geetest_challenge(这个 challenge 和最开始的不一样),所以传给哔哩哔哩 challenge 值也就不对了。

可以了,感谢大佬!

Lethairgo commented 7 months ago

手动验证器 的代码改一下就行,在 js代码的

validate.value = result.geetest_validate;
seccode.value = result.geetest_seccode;

后面加个 challenge.value = result.geetest_challenge;

把这个值也获取一下就行。

请问在哪里更改来获取这个值呀

Sualiu commented 7 months ago

手动验证器 的代码改一下就行,在 js代码的

validate.value = result.geetest_validate;
seccode.value = result.geetest_seccode;

后面加个 challenge.value = result.geetest_challenge; 把这个值也获取一下就行。

请问在哪里更改来获取这个值呀

在手动验证器的js里找类似于这种的代码: const result = captchaObj.getValidate();

从这个 result 变量里面获取到返回的新的 challenge 值,challenge.value = result.geetest_challenge

这个新的 challenge 同 validate, seccode值一同当做人机验证器的返回值。

若是失败了,或许可以考虑从人机验证器官网文档处下载新的 gt.js (http://docs.geetest.com/sensebot/deploy/client/web) 文件。记得不是太清楚了。

Lethairgo commented 7 months ago

手动验证器 的代码改一下就行,在 js代码的

validate.value = result.geetest_validate;
seccode.value = result.geetest_seccode;

后面加个 challenge.value = result.geetest_challenge; 把这个值也获取一下就行。

请问在哪里更改来获取这个值呀

在手动验证器的js里找类似于这种的代码: const result = captchaObj.getValidate();

从这个 result 变量里面获取到返回的新的 challenge 值,challenge.value = result.geetest_challenge

这个新的 challenge 同 validate, seccode值一同当做人机验证器的返回值。

若是失败了,或许可以考虑从人机验证器官网文档处下载新的 gt.js (http://docs.geetest.com/sensebot/deploy/client/web) 文件。记得不是太清楚了。

加了challenge.value = result.geetest_challenge;gt.js下载的最新的。 但是返回的challenge没有变化是为什么呢

Sualiu commented 7 months ago

手动验证器 的代码改一下就行,在 js代码的

validate.value = result.geetest_validate;
seccode.value = result.geetest_seccode;

后面加个 challenge.value = result.geetest_challenge; 把这个值也获取一下就行。

请问在哪里更改来获取这个值呀

在手动验证器的js里找类似于这种的代码: const result = captchaObj.getValidate(); 从这个 result 变量里面获取到返回的新的 challenge 值,challenge.value = result.geetest_challenge 这个新的 challenge 同 validate, seccode值一同当做人机验证器的返回值。 若是失败了,或许可以考虑从人机验证器官网文档处下载新的 gt.js (http://docs.geetest.com/sensebot/deploy/client/web) 文件。记得不是太清楚了。

加了challenge.value = result.geetest_challenge;gt.js下载的最新的。 但是返回的challenge没有变化是为什么呢

之前发现要加这个值好像就是因为他有时候不一样,不清楚这个值具体含义,反正加上后就没出过什么问题。