From 0219a0c8fe8dd877fc62bdb241601a27455f9ebc Mon Sep 17 00:00:00 2001 From: czp Date: Fri, 2 Feb 2018 14:39:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=B8=A6=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E7=A0=81=E7=9A=84=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 62 +++++++++ .../com/hiczp/bilibili/api/BilibiliAPI.java | 21 ++- .../bilibili/api/BilibiliSecurityHelper.java | 33 +++-- ...rrorResponseBodyConverterInterceptor.java} | 4 +- ...rorResponseStatusConverterInterceptor.java | 15 ++ .../api/passport/PassportService.java | 22 ++- .../exception/CaptchaMismatchException.java | 11 ++ .../bilibili/api/test/CaptchaInputDialog.form | 65 +++++++++ .../bilibili/api/test/CaptchaInputDialog.java | 131 ++++++++++++++++++ .../hiczp/bilibili/api/test/LoginTest.java | 24 +++- .../bilibili/api/test/SecurityHelperTest.java | 13 ++ 11 files changed, 380 insertions(+), 21 deletions(-) rename src/main/java/com/hiczp/bilibili/api/interceptor/{ErrorResponseConverterInterceptor.java => ErrorResponseBodyConverterInterceptor.java} (94%) create mode 100644 src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseStatusConverterInterceptor.java create mode 100644 src/main/java/com/hiczp/bilibili/api/passport/exception/CaptchaMismatchException.java create mode 100644 src/test/java/com/hiczp/bilibili/api/test/CaptchaInputDialog.form create mode 100644 src/test/java/com/hiczp/bilibili/api/test/CaptchaInputDialog.java diff --git a/README.md b/README.md index 229bb17..fd382a8 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ IOException 在网络故障时抛出 LoginException 在用户名密码不匹配时抛出 +CaptchaMismatchException 在验证码不正确时抛出, 见下文 [验证码问题](#验证码问题) 一节 + login 方法的返回值为 LoginResponseEntity 类型, 使用 .login(...).toBilibiliAccount() @@ -79,6 +81,66 @@ IOException 在网络故障时抛出 LoginException 在 accessToken 错误或过期时抛出 +### 验证码问题 +当对一个账户在短时间内(时长不明确)尝试多次错误的登录(密码错误)后, 再尝试登录该账号, 会被要求验证码. + +此时登录操作会抛出 CaptchaMismatchException 异常, 表示必须调用另一个接口 + + public LoginResponseEntity login(String username, + String password, + String captcha, + String cookie) throws IOException, LoginException, CaptchaMismatchException + +这个接口将带 captcha 参数地去登录, 注意这里还有一个 cookie 参数. + +下面先给出一段正确使用该接口的代码, 随后会解释其步骤 + + String username = "yourUsername"; + String password = "yourPassword"; + BilibiliAPI bilibiliAPI = new BilibiliAPI(); + try { + bilibiliAPI.login(username, password); + } catch (CaptchaMismatchException e) { //如果该账号现在需要验证码来进行登录, 就会抛出异常 + final cookie = "sid=123456"; //自己造一个 cookie 或者从服务器取得 + Response response = bilibiliAPI.getPassportService() + .getCaptcha(cookie) + .execute(); + InputStream inputStream = response.body().byteStream(); + String captcha = letUserInputCaptcha(inputStream); //让用户根据图片输入验证码 + bilibiliAPI.login( + username, + password, + captcha, + cookie + ); + } + +验证码是通过访问 https://passport.bilibili.com/captcha 这个地址获得的. + +访问这个地址需要带有一个 cookie, cookie 里面要有 "sid=xxx", 然后服务端会记录下对应关系, 也就是 sid xxx 对应验证码 yyy, 然后就可以验证了. + +我们会发现, 访问任何 passport.bilibili.com 下面的地址, 都会被分发一个 cookie, 里面带有 sid 的值. 我们访问 /captcha 也会被分发一个 cookie, 但是这个通过访问 captcha 而被分发得到的 cookie 和访问得到的验证码图片, 没有对应关系. 推测是因为 cookie 的发放在请求进入甚至模块运行完毕后才进行. + +所以我们如果不带 cookie 去访问 /captcha, 我们这样拿到的由 /captcha 返回的 cookie 和 验证码, 是不匹配的. + +所以我们要先从其他地方获取一个 cookie. + +我们可以用 /api/oauth2/getKey(获取加密密码用的 hash 和公钥) 来获取一个 cookie + + String cookie = bilibiliAPI.getPassportService() + .getKey() + .execute() + .headers() + .get("Set-cookie"); + +/captcha 不验证 cookie 正确性, 我们可以直接使用假的 cookie (比如 123456)对其发起验证码请求, 它会记录下这个假的 cookie 和 验证码 的对应关系, 一样能验证成功. 但是不推荐这么做. + +简单地说, 只要我们是带 cookie 访问 /captcha 的, 那么我们得到的验证码, 是和这个 cookie 绑定的. 我们接下去用这个 cookie 和 这个验证码的值 去进行带验证码的登录, 就可以成功登陆. + +至于验证码怎么处理, 可以显示给最终用户, 让用户来输入, 或者用一些预训练模型自动识别验证码. + +这个带验证码的登录接口也会继续抛出 CaptchaMismatchException, 如果验证码输入错误的话. + ### API 调用示例 打印一个直播间的历史弹幕 diff --git a/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java b/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java index 2027504..4dcda07 100644 --- a/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java +++ b/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java @@ -7,6 +7,7 @@ import com.hiczp.bilibili.api.passport.PassportService; import com.hiczp.bilibili.api.passport.entity.LoginResponseEntity; import com.hiczp.bilibili.api.passport.entity.LogoutResponseEntity; import com.hiczp.bilibili.api.passport.entity.RefreshTokenResponseEntity; +import com.hiczp.bilibili.api.passport.exception.CaptchaMismatchException; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import org.slf4j.Logger; @@ -20,7 +21,6 @@ import java.text.SimpleDateFormat; import java.time.Instant; import java.util.Date; -//TODO 尚未实现自动 refreshToken 的拦截器 public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider { private static final Logger LOGGER = LoggerFactory.getLogger(BilibiliAPI.class); @@ -66,7 +66,7 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider )) .addInterceptor(new AddAppKeyInterceptor(bilibiliClientProperties)) .addInterceptor(new SortParamsAndSignInterceptor(bilibiliClientProperties)) - .addInterceptor(new ErrorResponseConverterInterceptor()) + .addInterceptor(new ErrorResponseBodyConverterInterceptor()) .addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)) .build(); @@ -107,7 +107,7 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider .addInterceptor(new AddAccessKeyInterceptor(bilibiliAccount)) .addInterceptor(new AddAppKeyInterceptor(bilibiliClientProperties)) .addInterceptor(new SortParamsAndSignInterceptor(bilibiliClientProperties)) - .addInterceptor(new ErrorResponseConverterInterceptor()) + .addInterceptor(new ErrorResponseBodyConverterInterceptor()) .addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)) .build(); @@ -121,12 +121,21 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider return liveService; } - public LoginResponseEntity login(String username, String password) throws IOException, LoginException { + public LoginResponseEntity login(String username, String password) throws IOException, LoginException, CaptchaMismatchException { + return login(username, password, null, null); + } + + public LoginResponseEntity login(String username, + String password, + String captcha, + String cookie) throws IOException, LoginException, CaptchaMismatchException { LOGGER.info("Login attempting with username '{}'", username); LoginResponseEntity loginResponseEntity = BilibiliSecurityHelper.login( this, username, - password + password, + captcha, + cookie ); //判断返回值 switch (loginResponseEntity.getCode()) { @@ -141,7 +150,7 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider throw new LoginException("password error or hash expired"); } case ServerErrorCode.Passport.CAPTCHA_NOT_MATCH: { - throw new LoginException(loginResponseEntity.getMessage()); + throw new CaptchaMismatchException(loginResponseEntity.getMessage()); } default: { throw new IOException(loginResponseEntity.getMessage()); diff --git a/src/main/java/com/hiczp/bilibili/api/BilibiliSecurityHelper.java b/src/main/java/com/hiczp/bilibili/api/BilibiliSecurityHelper.java index 831e695..74a15c9 100644 --- a/src/main/java/com/hiczp/bilibili/api/BilibiliSecurityHelper.java +++ b/src/main/java/com/hiczp/bilibili/api/BilibiliSecurityHelper.java @@ -1,6 +1,5 @@ package com.hiczp.bilibili.api; -import com.hiczp.bilibili.api.passport.PassportService; import com.hiczp.bilibili.api.passport.entity.KeyEntity; import com.hiczp.bilibili.api.passport.entity.LoginResponseEntity; import com.hiczp.bilibili.api.passport.entity.LogoutResponseEntity; @@ -22,11 +21,9 @@ import java.util.Base64; import java.util.stream.Collectors; public class BilibiliSecurityHelper { - public static LoginResponseEntity login(BilibiliServiceProvider bilibiliServiceProvider, - String username, - String password) throws IOException { - PassportService passportService = bilibiliServiceProvider.getPassportService(); - KeyEntity keyEntity = passportService.getKey().execute().body(); + private static String cipherPassword(BilibiliServiceProvider bilibiliServiceProvider, + String password) throws IOException { + KeyEntity keyEntity = bilibiliServiceProvider.getPassportService().getKey().execute().body(); //服务器返回异常错误码 if (keyEntity.getCode() != 0) { throw new IOException(keyEntity.getMessage()); @@ -60,9 +57,27 @@ public class BilibiliSecurityHelper { } catch (InvalidKeyException e) { throw new IOException("get broken RSA public key"); } - //发起登录请求 - return passportService.login(username, cipheredPassword) - .execute() + return cipheredPassword; + } + + public static LoginResponseEntity login(BilibiliServiceProvider bilibiliServiceProvider, + String username, + String password) throws IOException { + return login(bilibiliServiceProvider, username, password, null, null); + } + + public static LoginResponseEntity login(BilibiliServiceProvider bilibiliServiceProvider, + String username, + String password, + String captcha, + String cookie) throws IOException { + return bilibiliServiceProvider.getPassportService() + .login( + username, + cipherPassword(bilibiliServiceProvider, password), + captcha, + cookie + ).execute() .body(); } diff --git a/src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseConverterInterceptor.java b/src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseBodyConverterInterceptor.java similarity index 94% rename from src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseConverterInterceptor.java rename to src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseBodyConverterInterceptor.java index 4162ec9..cb88b77 100644 --- a/src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseConverterInterceptor.java +++ b/src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseBodyConverterInterceptor.java @@ -15,8 +15,8 @@ import java.nio.charset.StandardCharsets; //由于服务器返回错误时的 data 字段类型不固定, 会导致 json 反序列化出错. //该拦截器将在返回的 code 不为 0 时, 将 response 转换为包含一个空 data 的 json 字符串. -public class ErrorResponseConverterInterceptor implements Interceptor { - private static final Logger LOGGER = LoggerFactory.getLogger(ErrorResponseConverterInterceptor.class); +public class ErrorResponseBodyConverterInterceptor implements Interceptor { + private static final Logger LOGGER = LoggerFactory.getLogger(ErrorResponseBodyConverterInterceptor.class); private static final JsonParser JSON_PARSER = new JsonParser(); private static final Gson GSON = new Gson(); diff --git a/src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseStatusConverterInterceptor.java b/src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseStatusConverterInterceptor.java new file mode 100644 index 0000000..7d65d89 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseStatusConverterInterceptor.java @@ -0,0 +1,15 @@ +package com.hiczp.bilibili.api.interceptor; + +import okhttp3.Interceptor; +import okhttp3.Response; + +import java.io.IOException; + +//当返回的数据中的 code 表示未登录时, 将 response 的 HttpStatus 改为 401, 以供 authenticator 使用 +//TODO 未实现 +public class ErrorResponseStatusConverterInterceptor implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/passport/PassportService.java b/src/main/java/com/hiczp/bilibili/api/passport/PassportService.java index 1e845a9..ec50e81 100644 --- a/src/main/java/com/hiczp/bilibili/api/passport/PassportService.java +++ b/src/main/java/com/hiczp/bilibili/api/passport/PassportService.java @@ -1,12 +1,30 @@ package com.hiczp.bilibili.api.passport; +import com.hiczp.bilibili.api.BaseUrlDefinition; import com.hiczp.bilibili.api.passport.entity.*; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Call; import retrofit2.http.GET; +import retrofit2.http.Header; import retrofit2.http.POST; import retrofit2.http.Query; public interface PassportService { + //获取验证码 + default okhttp3.Call getCaptcha(String cookies) { + return new OkHttpClient.Builder() + .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)) + .build() + .newCall( + new Request.Builder() + .url(BaseUrlDefinition.PASSPORT + "captcha") + .header("Cookie", cookies) + .build() + ); + } + @POST("api/oauth2/getKey") Call getKey(); @@ -14,10 +32,8 @@ public interface PassportService { Call login(@Query("username") String username, @Query("password") String password); //在一段时间内进行多次错误的登录, 将被要求输入验证码 - //TODO 尚不明确 captcha 是如何工作的 - @Deprecated @POST("api/oauth2/login") - Call loginWithCaptcha(@Query("username") String username, @Query("password") String password, @Query("captcha") String captcha); + Call login(@Query("username") String username, @Query("password") String password, @Query("captcha") String captcha, @Header("Cookie") String cookies); @GET("api/oauth2/info") Call getInfo(@Query("access_token") String accessToken); diff --git a/src/main/java/com/hiczp/bilibili/api/passport/exception/CaptchaMismatchException.java b/src/main/java/com/hiczp/bilibili/api/passport/exception/CaptchaMismatchException.java new file mode 100644 index 0000000..770353c --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/passport/exception/CaptchaMismatchException.java @@ -0,0 +1,11 @@ +package com.hiczp.bilibili.api.passport.exception; + +public class CaptchaMismatchException extends RuntimeException { + public CaptchaMismatchException() { + + } + + public CaptchaMismatchException(String message) { + super(message); + } +} diff --git a/src/test/java/com/hiczp/bilibili/api/test/CaptchaInputDialog.form b/src/test/java/com/hiczp/bilibili/api/test/CaptchaInputDialog.form new file mode 100644 index 0000000..7663090 --- /dev/null +++ b/src/test/java/com/hiczp/bilibili/api/test/CaptchaInputDialog.form @@ -0,0 +1,65 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/test/java/com/hiczp/bilibili/api/test/CaptchaInputDialog.java b/src/test/java/com/hiczp/bilibili/api/test/CaptchaInputDialog.java new file mode 100644 index 0000000..05b84a4 --- /dev/null +++ b/src/test/java/com/hiczp/bilibili/api/test/CaptchaInputDialog.java @@ -0,0 +1,131 @@ +package com.hiczp.bilibili.api.test; + +import com.hiczp.bilibili.api.BilibiliAPI; +import okhttp3.Response; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.io.IOException; + +public class CaptchaInputDialog extends JDialog { + private String cookie; + private String captcha; + + private JPanel contentPane; + private JButton buttonOK; + private JTextField textField; + private JLabel label; + + public CaptchaInputDialog() { + $$$setupUI$$$(); + setContentPane(contentPane); + setModal(true); + getRootPane().setDefaultButton(buttonOK); + buttonOK.addActionListener(e -> { + captcha = textField.getText(); + dispose(); + }); + } + + public static CaptchaInputDialog create() { + CaptchaInputDialog dialog = new CaptchaInputDialog(); + dialog.setTitle("Please input captcha"); + dialog.pack(); + dialog.setModal(true); + dialog.setVisible(true); + return dialog; + } + + public String getCookie() { + return cookie; + } + + public String getCaptcha() { + return captcha; + } + + private void createUIComponents() { + try { + cookie = new BilibiliAPI().getPassportService().getKey() + .execute() + .headers() + .get("Set-cookie"); + Response response = Config.getBilibiliAPI().getPassportService() + .getCaptcha(cookie) + .execute(); + if (response.code() != 200) { + throw new IOException(response.message()); + } + label = new JLabel(new ImageIcon(ImageIO.read(response.body().byteStream()))); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Method generated by IntelliJ IDEA GUI Designer + * >>> IMPORTANT!! <<< + * DO NOT edit this method OR call it in your code! + * + * @noinspection ALL + */ + private void $$$setupUI$$$() { + createUIComponents(); + contentPane = new JPanel(); + contentPane.setLayout(new GridBagLayout()); + contentPane.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2), null)); + final JPanel panel1 = new JPanel(); + panel1.setLayout(new GridBagLayout()); + panel1.setPreferredSize(new Dimension(200, 50)); + GridBagConstraints gbc; + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 1; + gbc.weightx = 1.0; + gbc.fill = GridBagConstraints.BOTH; + contentPane.add(panel1, gbc); + buttonOK = new JButton(); + buttonOK.setText("OK"); + gbc = new GridBagConstraints(); + gbc.gridx = 1; + gbc.gridy = 0; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.HORIZONTAL; + panel1.add(buttonOK, gbc); + textField = new JTextField(); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + panel1.add(textField, gbc); + final JPanel panel2 = new JPanel(); + panel2.setLayout(new GridBagLayout()); + panel2.setPreferredSize(new Dimension(200, 50)); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + contentPane.add(panel2, gbc); + label.setText("Label"); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.anchor = GridBagConstraints.WEST; + panel2.add(label, gbc); + } + + /** + * @noinspection ALL + */ + public JComponent $$$getRootComponent$$$() { + return contentPane; + } +} diff --git a/src/test/java/com/hiczp/bilibili/api/test/LoginTest.java b/src/test/java/com/hiczp/bilibili/api/test/LoginTest.java index 03fad96..9606648 100644 --- a/src/test/java/com/hiczp/bilibili/api/test/LoginTest.java +++ b/src/test/java/com/hiczp/bilibili/api/test/LoginTest.java @@ -1,12 +1,34 @@ package com.hiczp.bilibili.api.test; +import com.hiczp.bilibili.api.passport.exception.CaptchaMismatchException; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.*; public class LoginTest { + private static final Logger LOGGER = LoggerFactory.getLogger(LoginTest.class); private static final Config CONFIG = Config.getInstance(); @Test public void login() throws Exception { - Config.getBilibiliAPI().login(CONFIG.getUsername(), CONFIG.getPassword()); + try { + Config.getBilibiliAPI().login(CONFIG.getUsername(), CONFIG.getPassword()); + } catch (CaptchaMismatchException e) { + LOGGER.info("Need captcha"); + if (GraphicsEnvironment.isHeadless()) { + LOGGER.error("Need graphics support to display captcha, login failed"); + throw new UnsupportedOperationException(e); + } else { + CaptchaInputDialog captchaInputDialog = CaptchaInputDialog.create(); + Config.getBilibiliAPI().login( + CONFIG.getUsername(), + CONFIG.getPassword(), + captchaInputDialog.getCaptcha(), + captchaInputDialog.getCookie() + ); + } + } } } diff --git a/src/test/java/com/hiczp/bilibili/api/test/SecurityHelperTest.java b/src/test/java/com/hiczp/bilibili/api/test/SecurityHelperTest.java index 885d666..6a12a53 100644 --- a/src/test/java/com/hiczp/bilibili/api/test/SecurityHelperTest.java +++ b/src/test/java/com/hiczp/bilibili/api/test/SecurityHelperTest.java @@ -4,6 +4,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.hiczp.bilibili.api.BilibiliAPI; import com.hiczp.bilibili.api.BilibiliSecurityHelper; +import com.hiczp.bilibili.api.ServerErrorCode; import com.hiczp.bilibili.api.passport.entity.LoginResponseEntity; import com.hiczp.bilibili.api.passport.entity.RefreshTokenResponseEntity; import org.junit.Test; @@ -22,6 +23,10 @@ public class SecurityHelperTest { CONFIG.getUsername(), CONFIG.getPassword() ); + if (loginResponseEntity.getCode() == ServerErrorCode.Passport.CAPTCHA_NOT_MATCH) { + LOGGER.error("This account need captcha to login, ignore test"); + return; + } LOGGER.info("{}", GSON.toJson(loginResponseEntity)); BilibiliSecurityHelper.logout(new BilibiliAPI(), loginResponseEntity.getData().getAccessToken()); } @@ -43,6 +48,10 @@ public class SecurityHelperTest { CONFIG.getUsername(), CONFIG.getPassword() ); + if (loginResponseEntity.getCode() == ServerErrorCode.Passport.CAPTCHA_NOT_MATCH) { + LOGGER.error("This account need captcha to login, ignore test"); + return; + } RefreshTokenResponseEntity refreshTokenResponseEntity = BilibiliSecurityHelper.refreshToken( new BilibiliAPI(), loginResponseEntity.getData().getAccessToken(), @@ -69,6 +78,10 @@ public class SecurityHelperTest { CONFIG.getUsername(), CONFIG.getPassword() ); + if (loginResponseEntity.getCode() == ServerErrorCode.Passport.CAPTCHA_NOT_MATCH) { + LOGGER.error("This account need captcha to login, ignore test"); + return; + } String accessToken = loginResponseEntity.getData().getAccessToken(); RefreshTokenResponseEntity refreshTokenResponseEntity = BilibiliSecurityHelper.refreshToken( new BilibiliAPI(),