From 756cdf4b5827e4ba9573c9374cf2787c015be741 Mon Sep 17 00:00:00 2001 From: czp Date: Sun, 4 Feb 2018 23:31:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BD=93=20token=20=E8=BF=87=E6=9C=9F=E6=97=B6?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=BF=9B=E8=A1=8C=20refreshToken=20=E6=93=8D?= =?UTF-8?q?=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/hiczp/bilibili/api/BilibiliAPI.java | 62 ++++++++++++++----- .../interceptor/AddAccessKeyInterceptor.java | 6 +- ...=> ErrorResponseConverterInterceptor.java} | 16 +---- ...rorResponseStatusConverterInterceptor.java | 15 ----- .../api/interceptor/InterceptorHelper.java | 26 ++++++++ .../interceptor/RefreshTokenInterceptor.java | 58 +++++++++++++++++ .../SortParamsAndSignInterceptor.java | 22 ++++--- .../bilibili/api/test/AuthenticatorTest.java | 26 ++++++++ .../hiczp/bilibili/api/test/RuleSuite.java | 1 + .../bilibili/api/test/SecurityHelperTest.java | 2 + 10 files changed, 179 insertions(+), 55 deletions(-) rename src/main/java/com/hiczp/bilibili/api/interceptor/{ErrorResponseBodyConverterInterceptor.java => ErrorResponseConverterInterceptor.java} (72%) delete mode 100644 src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseStatusConverterInterceptor.java create mode 100644 src/main/java/com/hiczp/bilibili/api/interceptor/InterceptorHelper.java create mode 100644 src/main/java/com/hiczp/bilibili/api/interceptor/RefreshTokenInterceptor.java create mode 100644 src/test/java/com/hiczp/bilibili/api/test/AuthenticatorTest.java diff --git a/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java b/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java index 4dcda07..dcfbd8c 100644 --- a/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java +++ b/src/main/java/com/hiczp/bilibili/api/BilibiliAPI.java @@ -28,6 +28,10 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider private final BilibiliClientProperties bilibiliClientProperties; private final BilibiliAccount bilibiliAccount; + //用于阻止进行多次错误的 refreshToken 操作 + private String invalidToken; + private String invalidRefreshToken; + private PassportService passportService; private LiveService liveService; @@ -43,12 +47,12 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider public BilibiliAPI(BilibiliAccount bilibiliAccount) { this.bilibiliClientProperties = BilibiliClientProperties.defaultSetting(); - this.bilibiliAccount = bilibiliAccount; + this.bilibiliAccount = new BilibiliAccount(bilibiliAccount); } public BilibiliAPI(BilibiliClientProperties bilibiliClientProperties, BilibiliAccount bilibiliAccount) { this.bilibiliClientProperties = bilibiliClientProperties; - this.bilibiliAccount = bilibiliAccount; + this.bilibiliAccount = new BilibiliAccount(bilibiliAccount); } //TODO 不明确客户端访问 passport.bilibili.com 时使用的 UA @@ -66,7 +70,7 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider )) .addInterceptor(new AddAppKeyInterceptor(bilibiliClientProperties)) .addInterceptor(new SortParamsAndSignInterceptor(bilibiliClientProperties)) - .addInterceptor(new ErrorResponseBodyConverterInterceptor()) + .addInterceptor(new ErrorResponseConverterInterceptor()) .addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)) .build(); @@ -88,10 +92,12 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider "Buvid", bilibiliClientProperties.getBuvId(), "User-Agent", "Mozilla/5.0 BiliDroid/5.15.0 (bbcallen@gmail.com)", "Device-ID", bilibiliClientProperties.getHardwareId() - )).addInterceptor(new AddDynamicHeadersInterceptor( + )) + .addInterceptor(new AddDynamicHeadersInterceptor( //Display-ID 的值在未登录前为 Buvid-客户端启动时间, 在登录后为 mid-客户端启动时间 () -> "Display-ID", () -> String.format("%s-%d", bilibiliAccount.getUserId() == null ? bilibiliClientProperties.getBuvId() : bilibiliAccount.getUserId(), apiInitTime) - )).addInterceptor(new AddFixedParamsInterceptor( + )) + .addInterceptor(new AddFixedParamsInterceptor( "_device", "android", "_hwid", bilibiliClientProperties.getHardwareId(), "build", bilibiliClientProperties.getBuild(), @@ -100,14 +106,22 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider "scale", bilibiliClientProperties.getScale(), "src", "google", "version", bilibiliClientProperties.getVersion() - )).addInterceptor(new AddDynamicParamsInterceptor( + )) + .addInterceptor(new AddDynamicParamsInterceptor( () -> "ts", () -> Long.toString(Instant.now().getEpochSecond()), () -> "trace_id", () -> new SimpleDateFormat("yyyyMMddHHmm000ss").format(new Date()) )) - .addInterceptor(new AddAccessKeyInterceptor(bilibiliAccount)) .addInterceptor(new AddAppKeyInterceptor(bilibiliClientProperties)) + .addInterceptor(new RefreshTokenInterceptor( + this, + ServerErrorCode.Common.UNAUTHORIZED, + ServerErrorCode.Live.USER_NO_LOGIN, + ServerErrorCode.Live.PLEASE_LOGIN, + ServerErrorCode.Live.NO_LOGIN + )) + .addInterceptor(new AddAccessKeyInterceptor(bilibiliAccount)) .addInterceptor(new SortParamsAndSignInterceptor(bilibiliClientProperties)) - .addInterceptor(new ErrorResponseBodyConverterInterceptor()) + .addInterceptor(new ErrorResponseConverterInterceptor()) .addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)) .build(); @@ -125,10 +139,10 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider return login(username, password, null, null); } - public LoginResponseEntity login(String username, - String password, - String captcha, - String cookie) throws IOException, LoginException, CaptchaMismatchException { + public synchronized 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, @@ -150,7 +164,7 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider throw new LoginException("password error or hash expired"); } case ServerErrorCode.Passport.CAPTCHA_NOT_MATCH: { - throw new CaptchaMismatchException(loginResponseEntity.getMessage()); + throw new CaptchaMismatchException("captcha mismatch"); } default: { throw new IOException(loginResponseEntity.getMessage()); @@ -161,7 +175,11 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider return loginResponseEntity; } - public RefreshTokenResponseEntity refreshToken() throws IOException, LoginException { + public synchronized RefreshTokenResponseEntity refreshToken() throws IOException, LoginException { + if (isCurrentTokenAndRefreshTokenInvalid()) { + throw new LoginException("access token or refresh token not been set yet or invalid"); + } + LOGGER.info("RefreshToken attempting with userId '{}'", bilibiliAccount.getUserId()); RefreshTokenResponseEntity refreshTokenResponseEntity = BilibiliSecurityHelper.refreshToken( this, @@ -174,9 +192,11 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider } break; case ServerErrorCode.Passport.NO_LOGIN: { + markCurrentTokenAndRefreshTokenInvalid(); throw new LoginException("access token invalid"); } case ServerErrorCode.Passport.REFRESH_TOKEN_NOT_MATCH: { + markCurrentTokenAndRefreshTokenInvalid(); throw new LoginException("access token and refresh token mismatch"); } default: { @@ -188,7 +208,7 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider return refreshTokenResponseEntity; } - public LogoutResponseEntity logout() throws IOException, LoginException { + public synchronized LogoutResponseEntity logout() throws IOException, LoginException { LOGGER.info("Logout attempting with userId '{}'", bilibiliAccount.getUserId()); Long userId = bilibiliAccount.getUserId(); LogoutResponseEntity logoutResponseEntity = BilibiliSecurityHelper.logout(this, bilibiliAccount.getAccessToken()); @@ -216,6 +236,18 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider new LiveClient(this, showRoomId, bilibiliAccount.getUserId()); } + private void markCurrentTokenAndRefreshTokenInvalid() { + invalidToken = bilibiliAccount.getAccessToken(); + invalidRefreshToken = bilibiliAccount.getRefreshToken(); + } + + public boolean isCurrentTokenAndRefreshTokenInvalid() { + //如果 accessToken 或 refreshToken 没有被设置或者已经尝试过并明确他们是无效的 + return bilibiliAccount.getAccessToken() == null || + bilibiliAccount.getRefreshToken() == null || + (bilibiliAccount.getAccessToken().equals(invalidToken) && bilibiliAccount.getRefreshToken().equals(invalidRefreshToken)); + } + public BilibiliClientProperties getBilibiliClientProperties() { return bilibiliClientProperties; } diff --git a/src/main/java/com/hiczp/bilibili/api/interceptor/AddAccessKeyInterceptor.java b/src/main/java/com/hiczp/bilibili/api/interceptor/AddAccessKeyInterceptor.java index 9d4eb4c..f33f103 100644 --- a/src/main/java/com/hiczp/bilibili/api/interceptor/AddAccessKeyInterceptor.java +++ b/src/main/java/com/hiczp/bilibili/api/interceptor/AddAccessKeyInterceptor.java @@ -1,5 +1,6 @@ package com.hiczp.bilibili.api.interceptor; +import com.google.common.base.Strings; import com.hiczp.bilibili.api.BilibiliSecurityContext; import okhttp3.HttpUrl; import okhttp3.Interceptor; @@ -20,8 +21,9 @@ public class AddAccessKeyInterceptor implements Interceptor { Request request = chain.request(); HttpUrl.Builder httpUrlBuilder = request.url().newBuilder(); String accessKey = bilibiliSecurityContext.getAccessToken(); - if (accessKey != null && accessKey.length() != 0) { - httpUrlBuilder.addQueryParameter("access_key", accessKey); + if (!Strings.isNullOrEmpty(accessKey)) { + httpUrlBuilder.removeAllQueryParameters("access_key") + .addQueryParameter("access_key", accessKey); } return chain.proceed(request.newBuilder().url(httpUrlBuilder.build()).build()); } diff --git a/src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseBodyConverterInterceptor.java b/src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseConverterInterceptor.java similarity index 72% rename from src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseBodyConverterInterceptor.java rename to src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseConverterInterceptor.java index cb88b77..4cdb1e0 100644 --- a/src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseBodyConverterInterceptor.java +++ b/src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseConverterInterceptor.java @@ -5,19 +5,15 @@ import com.hiczp.bilibili.api.ServerErrorCode; import okhttp3.Interceptor; import okhttp3.Response; import okhttp3.ResponseBody; -import okio.Buffer; -import okio.BufferedSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.nio.charset.StandardCharsets; //由于服务器返回错误时的 data 字段类型不固定, 会导致 json 反序列化出错. //该拦截器将在返回的 code 不为 0 时, 将 response 转换为包含一个空 data 的 json 字符串. -public class ErrorResponseBodyConverterInterceptor implements Interceptor { - private static final Logger LOGGER = LoggerFactory.getLogger(ErrorResponseBodyConverterInterceptor.class); - private static final JsonParser JSON_PARSER = new JsonParser(); +public class ErrorResponseConverterInterceptor implements Interceptor { + private static final Logger LOGGER = LoggerFactory.getLogger(ErrorResponseConverterInterceptor.class); private static final Gson GSON = new Gson(); @Override @@ -25,13 +21,7 @@ public class ErrorResponseBodyConverterInterceptor implements Interceptor { Response response = chain.proceed(chain.request()); ResponseBody responseBody = response.body(); - BufferedSource bufferedSource = responseBody.source(); - bufferedSource.request(Long.MAX_VALUE); - Buffer buffer = bufferedSource.buffer(); - //必须要 clone 一次, 否则将导致流关闭 - String json = buffer.clone().readString(StandardCharsets.UTF_8); - - JsonObject jsonObject = JSON_PARSER.parse(json).getAsJsonObject(); + JsonObject jsonObject = InterceptorHelper.getJsonInBody(response); JsonElement code = jsonObject.get("code"); //code 字段不存在 if (code == null) { diff --git a/src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseStatusConverterInterceptor.java b/src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseStatusConverterInterceptor.java deleted file mode 100644 index 7d65d89..0000000 --- a/src/main/java/com/hiczp/bilibili/api/interceptor/ErrorResponseStatusConverterInterceptor.java +++ /dev/null @@ -1,15 +0,0 @@ -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/interceptor/InterceptorHelper.java b/src/main/java/com/hiczp/bilibili/api/interceptor/InterceptorHelper.java new file mode 100644 index 0000000..b6805e6 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/interceptor/InterceptorHelper.java @@ -0,0 +1,26 @@ +package com.hiczp.bilibili.api.interceptor; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +class InterceptorHelper { + private static final JsonParser JSON_PARSER = new JsonParser(); + + static JsonObject getJsonInBody(Response response) throws IOException { + ResponseBody responseBody = response.body(); + BufferedSource bufferedSource = responseBody.source(); + bufferedSource.request(Long.MAX_VALUE); + Buffer buffer = bufferedSource.buffer(); + return JSON_PARSER.parse( + //必须要 clone 一次, 否则将导致流关闭 + buffer.clone().readString(StandardCharsets.UTF_8) + ).getAsJsonObject(); + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/interceptor/RefreshTokenInterceptor.java b/src/main/java/com/hiczp/bilibili/api/interceptor/RefreshTokenInterceptor.java new file mode 100644 index 0000000..e30db62 --- /dev/null +++ b/src/main/java/com/hiczp/bilibili/api/interceptor/RefreshTokenInterceptor.java @@ -0,0 +1,58 @@ +package com.hiczp.bilibili.api.interceptor; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.hiczp.bilibili.api.BilibiliAPI; +import com.hiczp.bilibili.api.ServerErrorCode; +import okhttp3.Interceptor; +import okhttp3.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.stream.IntStream; + +//自动刷新 token +public class RefreshTokenInterceptor implements Interceptor { + private static final Logger LOGGER = LoggerFactory.getLogger(RefreshTokenInterceptor.class); + + private BilibiliAPI bilibiliAPI; + private int[] codes; + + public RefreshTokenInterceptor(BilibiliAPI bilibiliAPI, int... codes) { + this.bilibiliAPI = bilibiliAPI; + this.codes = codes; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Response response = chain.proceed(chain.request()); + + JsonObject jsonObject = InterceptorHelper.getJsonInBody(response); + JsonElement codeElement = jsonObject.get("code"); + if (codeElement == null) { + return response; + } + int codeValue = codeElement.getAsInt(); + if (codeValue == ServerErrorCode.Common.OK) { + return response; + } + + if (IntStream.of(codes).noneMatch(code -> code == codeValue)) { + return response; + } + + if (bilibiliAPI.isCurrentTokenAndRefreshTokenInvalid()) { + return response; + } + + try { + bilibiliAPI.refreshToken(); + response = chain.proceed(chain.request()); + } catch (Exception e) { + LOGGER.error("refresh token failed: {}", e.getMessage()); + } + + return response; + } +} diff --git a/src/main/java/com/hiczp/bilibili/api/interceptor/SortParamsAndSignInterceptor.java b/src/main/java/com/hiczp/bilibili/api/interceptor/SortParamsAndSignInterceptor.java index c820a38..e21539f 100644 --- a/src/main/java/com/hiczp/bilibili/api/interceptor/SortParamsAndSignInterceptor.java +++ b/src/main/java/com/hiczp/bilibili/api/interceptor/SortParamsAndSignInterceptor.java @@ -30,16 +30,18 @@ public class SortParamsAndSignInterceptor implements Interceptor { Request request = chain.request(); HttpUrl httpUrl = request.url(); List nameAndValues = new ArrayList<>(httpUrl.querySize() + 1); - httpUrl.queryParameterNames().forEach(name -> - httpUrl.queryParameterValues(name).forEach(value -> { - try { - nameAndValues.add(String.format("%s=%s", name, URLEncoder.encode(value, StandardCharsets.UTF_8.toString()))); - } catch (UnsupportedEncodingException e) { - throw new Error(e); - } - } - ) - ); + httpUrl.queryParameterNames().stream() + .filter(parameterName -> !parameterName.equals("sign")) + .forEach(name -> + httpUrl.queryParameterValues(name).forEach(value -> { + try { + nameAndValues.add(String.format("%s=%s", name, URLEncoder.encode(value, StandardCharsets.UTF_8.toString()))); + } catch (UnsupportedEncodingException e) { + throw new Error(e); + } + } + ) + ); Collections.sort(nameAndValues); nameAndValues.add(String.format("%s=%s", "sign", calculateSign(nameAndValues))); return chain.proceed( diff --git a/src/test/java/com/hiczp/bilibili/api/test/AuthenticatorTest.java b/src/test/java/com/hiczp/bilibili/api/test/AuthenticatorTest.java new file mode 100644 index 0000000..b612583 --- /dev/null +++ b/src/test/java/com/hiczp/bilibili/api/test/AuthenticatorTest.java @@ -0,0 +1,26 @@ +package com.hiczp.bilibili.api.test; + +import com.hiczp.bilibili.api.BilibiliAPI; +import com.hiczp.bilibili.api.BilibiliAccount; +import org.junit.Test; + +public class AuthenticatorTest { + @Test + public void test() throws Exception { + BilibiliAPI bilibiliAPI = new BilibiliAPI( + new BilibiliAccount( + "123", + "123", + null, + null, + null + ) + ); + bilibiliAPI.getLiveService() + .getPlayerBag() + .execute(); + bilibiliAPI.getLiveService() + .getPlayerBag() + .execute(); + } +} diff --git a/src/test/java/com/hiczp/bilibili/api/test/RuleSuite.java b/src/test/java/com/hiczp/bilibili/api/test/RuleSuite.java index f8d000b..b9a3b11 100644 --- a/src/test/java/com/hiczp/bilibili/api/test/RuleSuite.java +++ b/src/test/java/com/hiczp/bilibili/api/test/RuleSuite.java @@ -18,6 +18,7 @@ import java.io.InputStreamReader; SsoTest.class, SendBulletScreenTest.class, SecurityHelperTest.class, + AuthenticatorTest.class, LogoutTest.class }) public class RuleSuite { 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 6a12a53..a09eed1 100644 --- a/src/test/java/com/hiczp/bilibili/api/test/SecurityHelperTest.java +++ b/src/test/java/com/hiczp/bilibili/api/test/SecurityHelperTest.java @@ -7,6 +7,7 @@ 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.Ignore; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,6 +32,7 @@ public class SecurityHelperTest { BilibiliSecurityHelper.logout(new BilibiliAPI(), loginResponseEntity.getData().getAccessToken()); } + @Ignore @Test public void loginWithWrongUsername() throws Exception { LoginResponseEntity loginResponseEntity = BilibiliSecurityHelper.login(