当 token 过期时自动进行 refreshToken 操作

This commit is contained in:
czp 2018-02-04 23:31:11 +08:00
parent 2227237f68
commit 756cdf4b58
10 changed files with 179 additions and 55 deletions

View File

@ -28,6 +28,10 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider
private final BilibiliClientProperties bilibiliClientProperties; private final BilibiliClientProperties bilibiliClientProperties;
private final BilibiliAccount bilibiliAccount; private final BilibiliAccount bilibiliAccount;
//用于阻止进行多次错误的 refreshToken 操作
private String invalidToken;
private String invalidRefreshToken;
private PassportService passportService; private PassportService passportService;
private LiveService liveService; private LiveService liveService;
@ -43,12 +47,12 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider
public BilibiliAPI(BilibiliAccount bilibiliAccount) { public BilibiliAPI(BilibiliAccount bilibiliAccount) {
this.bilibiliClientProperties = BilibiliClientProperties.defaultSetting(); this.bilibiliClientProperties = BilibiliClientProperties.defaultSetting();
this.bilibiliAccount = bilibiliAccount; this.bilibiliAccount = new BilibiliAccount(bilibiliAccount);
} }
public BilibiliAPI(BilibiliClientProperties bilibiliClientProperties, BilibiliAccount bilibiliAccount) { public BilibiliAPI(BilibiliClientProperties bilibiliClientProperties, BilibiliAccount bilibiliAccount) {
this.bilibiliClientProperties = bilibiliClientProperties; this.bilibiliClientProperties = bilibiliClientProperties;
this.bilibiliAccount = bilibiliAccount; this.bilibiliAccount = new BilibiliAccount(bilibiliAccount);
} }
//TODO 不明确客户端访问 passport.bilibili.com 时使用的 UA //TODO 不明确客户端访问 passport.bilibili.com 时使用的 UA
@ -66,7 +70,7 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider
)) ))
.addInterceptor(new AddAppKeyInterceptor(bilibiliClientProperties)) .addInterceptor(new AddAppKeyInterceptor(bilibiliClientProperties))
.addInterceptor(new SortParamsAndSignInterceptor(bilibiliClientProperties)) .addInterceptor(new SortParamsAndSignInterceptor(bilibiliClientProperties))
.addInterceptor(new ErrorResponseBodyConverterInterceptor()) .addInterceptor(new ErrorResponseConverterInterceptor())
.addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)) .addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC))
.build(); .build();
@ -88,10 +92,12 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider
"Buvid", bilibiliClientProperties.getBuvId(), "Buvid", bilibiliClientProperties.getBuvId(),
"User-Agent", "Mozilla/5.0 BiliDroid/5.15.0 (bbcallen@gmail.com)", "User-Agent", "Mozilla/5.0 BiliDroid/5.15.0 (bbcallen@gmail.com)",
"Device-ID", bilibiliClientProperties.getHardwareId() "Device-ID", bilibiliClientProperties.getHardwareId()
)).addInterceptor(new AddDynamicHeadersInterceptor( ))
.addInterceptor(new AddDynamicHeadersInterceptor(
//Display-ID 的值在未登录前为 Buvid-客户端启动时间, 在登录后为 mid-客户端启动时间 //Display-ID 的值在未登录前为 Buvid-客户端启动时间, 在登录后为 mid-客户端启动时间
() -> "Display-ID", () -> String.format("%s-%d", bilibiliAccount.getUserId() == null ? bilibiliClientProperties.getBuvId() : bilibiliAccount.getUserId(), apiInitTime) () -> "Display-ID", () -> String.format("%s-%d", bilibiliAccount.getUserId() == null ? bilibiliClientProperties.getBuvId() : bilibiliAccount.getUserId(), apiInitTime)
)).addInterceptor(new AddFixedParamsInterceptor( ))
.addInterceptor(new AddFixedParamsInterceptor(
"_device", "android", "_device", "android",
"_hwid", bilibiliClientProperties.getHardwareId(), "_hwid", bilibiliClientProperties.getHardwareId(),
"build", bilibiliClientProperties.getBuild(), "build", bilibiliClientProperties.getBuild(),
@ -100,14 +106,22 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider
"scale", bilibiliClientProperties.getScale(), "scale", bilibiliClientProperties.getScale(),
"src", "google", "src", "google",
"version", bilibiliClientProperties.getVersion() "version", bilibiliClientProperties.getVersion()
)).addInterceptor(new AddDynamicParamsInterceptor( ))
.addInterceptor(new AddDynamicParamsInterceptor(
() -> "ts", () -> Long.toString(Instant.now().getEpochSecond()), () -> "ts", () -> Long.toString(Instant.now().getEpochSecond()),
() -> "trace_id", () -> new SimpleDateFormat("yyyyMMddHHmm000ss").format(new Date()) () -> "trace_id", () -> new SimpleDateFormat("yyyyMMddHHmm000ss").format(new Date())
)) ))
.addInterceptor(new AddAccessKeyInterceptor(bilibiliAccount))
.addInterceptor(new AddAppKeyInterceptor(bilibiliClientProperties)) .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 SortParamsAndSignInterceptor(bilibiliClientProperties))
.addInterceptor(new ErrorResponseBodyConverterInterceptor()) .addInterceptor(new ErrorResponseConverterInterceptor())
.addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)) .addNetworkInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC))
.build(); .build();
@ -125,7 +139,7 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider
return login(username, password, null, null); return login(username, password, null, null);
} }
public LoginResponseEntity login(String username, public synchronized LoginResponseEntity login(String username,
String password, String password,
String captcha, String captcha,
String cookie) throws IOException, LoginException, CaptchaMismatchException { String cookie) throws IOException, LoginException, CaptchaMismatchException {
@ -150,7 +164,7 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider
throw new LoginException("password error or hash expired"); throw new LoginException("password error or hash expired");
} }
case ServerErrorCode.Passport.CAPTCHA_NOT_MATCH: { case ServerErrorCode.Passport.CAPTCHA_NOT_MATCH: {
throw new CaptchaMismatchException(loginResponseEntity.getMessage()); throw new CaptchaMismatchException("captcha mismatch");
} }
default: { default: {
throw new IOException(loginResponseEntity.getMessage()); throw new IOException(loginResponseEntity.getMessage());
@ -161,7 +175,11 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider
return loginResponseEntity; 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()); LOGGER.info("RefreshToken attempting with userId '{}'", bilibiliAccount.getUserId());
RefreshTokenResponseEntity refreshTokenResponseEntity = BilibiliSecurityHelper.refreshToken( RefreshTokenResponseEntity refreshTokenResponseEntity = BilibiliSecurityHelper.refreshToken(
this, this,
@ -174,9 +192,11 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider
} }
break; break;
case ServerErrorCode.Passport.NO_LOGIN: { case ServerErrorCode.Passport.NO_LOGIN: {
markCurrentTokenAndRefreshTokenInvalid();
throw new LoginException("access token invalid"); throw new LoginException("access token invalid");
} }
case ServerErrorCode.Passport.REFRESH_TOKEN_NOT_MATCH: { case ServerErrorCode.Passport.REFRESH_TOKEN_NOT_MATCH: {
markCurrentTokenAndRefreshTokenInvalid();
throw new LoginException("access token and refresh token mismatch"); throw new LoginException("access token and refresh token mismatch");
} }
default: { default: {
@ -188,7 +208,7 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider
return refreshTokenResponseEntity; return refreshTokenResponseEntity;
} }
public LogoutResponseEntity logout() throws IOException, LoginException { public synchronized LogoutResponseEntity logout() throws IOException, LoginException {
LOGGER.info("Logout attempting with userId '{}'", bilibiliAccount.getUserId()); LOGGER.info("Logout attempting with userId '{}'", bilibiliAccount.getUserId());
Long userId = bilibiliAccount.getUserId(); Long userId = bilibiliAccount.getUserId();
LogoutResponseEntity logoutResponseEntity = BilibiliSecurityHelper.logout(this, bilibiliAccount.getAccessToken()); LogoutResponseEntity logoutResponseEntity = BilibiliSecurityHelper.logout(this, bilibiliAccount.getAccessToken());
@ -216,6 +236,18 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider
new LiveClient(this, showRoomId, bilibiliAccount.getUserId()); 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() { public BilibiliClientProperties getBilibiliClientProperties() {
return bilibiliClientProperties; return bilibiliClientProperties;
} }

View File

@ -1,5 +1,6 @@
package com.hiczp.bilibili.api.interceptor; package com.hiczp.bilibili.api.interceptor;
import com.google.common.base.Strings;
import com.hiczp.bilibili.api.BilibiliSecurityContext; import com.hiczp.bilibili.api.BilibiliSecurityContext;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import okhttp3.Interceptor; import okhttp3.Interceptor;
@ -20,8 +21,9 @@ public class AddAccessKeyInterceptor implements Interceptor {
Request request = chain.request(); Request request = chain.request();
HttpUrl.Builder httpUrlBuilder = request.url().newBuilder(); HttpUrl.Builder httpUrlBuilder = request.url().newBuilder();
String accessKey = bilibiliSecurityContext.getAccessToken(); String accessKey = bilibiliSecurityContext.getAccessToken();
if (accessKey != null && accessKey.length() != 0) { if (!Strings.isNullOrEmpty(accessKey)) {
httpUrlBuilder.addQueryParameter("access_key", accessKey); httpUrlBuilder.removeAllQueryParameters("access_key")
.addQueryParameter("access_key", accessKey);
} }
return chain.proceed(request.newBuilder().url(httpUrlBuilder.build()).build()); return chain.proceed(request.newBuilder().url(httpUrlBuilder.build()).build());
} }

View File

@ -5,19 +5,15 @@ import com.hiczp.bilibili.api.ServerErrorCode;
import okhttp3.Interceptor; import okhttp3.Interceptor;
import okhttp3.Response; import okhttp3.Response;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSource;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
//由于服务器返回错误时的 data 字段类型不固定, 会导致 json 反序列化出错. //由于服务器返回错误时的 data 字段类型不固定, 会导致 json 反序列化出错.
//该拦截器将在返回的 code 不为 0 , response 转换为包含一个空 data json 字符串. //该拦截器将在返回的 code 不为 0 , response 转换为包含一个空 data json 字符串.
public class ErrorResponseBodyConverterInterceptor implements Interceptor { public class ErrorResponseConverterInterceptor implements Interceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(ErrorResponseBodyConverterInterceptor.class); private static final Logger LOGGER = LoggerFactory.getLogger(ErrorResponseConverterInterceptor.class);
private static final JsonParser JSON_PARSER = new JsonParser();
private static final Gson GSON = new Gson(); private static final Gson GSON = new Gson();
@Override @Override
@ -25,13 +21,7 @@ public class ErrorResponseBodyConverterInterceptor implements Interceptor {
Response response = chain.proceed(chain.request()); Response response = chain.proceed(chain.request());
ResponseBody responseBody = response.body(); ResponseBody responseBody = response.body();
BufferedSource bufferedSource = responseBody.source(); JsonObject jsonObject = InterceptorHelper.getJsonInBody(response);
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();
JsonElement code = jsonObject.get("code"); JsonElement code = jsonObject.get("code");
//code 字段不存在 //code 字段不存在
if (code == null) { if (code == null) {

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -30,7 +30,9 @@ public class SortParamsAndSignInterceptor implements Interceptor {
Request request = chain.request(); Request request = chain.request();
HttpUrl httpUrl = request.url(); HttpUrl httpUrl = request.url();
List<String> nameAndValues = new ArrayList<>(httpUrl.querySize() + 1); List<String> nameAndValues = new ArrayList<>(httpUrl.querySize() + 1);
httpUrl.queryParameterNames().forEach(name -> httpUrl.queryParameterNames().stream()
.filter(parameterName -> !parameterName.equals("sign"))
.forEach(name ->
httpUrl.queryParameterValues(name).forEach(value -> { httpUrl.queryParameterValues(name).forEach(value -> {
try { try {
nameAndValues.add(String.format("%s=%s", name, URLEncoder.encode(value, StandardCharsets.UTF_8.toString()))); nameAndValues.add(String.format("%s=%s", name, URLEncoder.encode(value, StandardCharsets.UTF_8.toString())));

View File

@ -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();
}
}

View File

@ -18,6 +18,7 @@ import java.io.InputStreamReader;
SsoTest.class, SsoTest.class,
SendBulletScreenTest.class, SendBulletScreenTest.class,
SecurityHelperTest.class, SecurityHelperTest.class,
AuthenticatorTest.class,
LogoutTest.class LogoutTest.class
}) })
public class RuleSuite { public class RuleSuite {

View File

@ -7,6 +7,7 @@ import com.hiczp.bilibili.api.BilibiliSecurityHelper;
import com.hiczp.bilibili.api.ServerErrorCode; import com.hiczp.bilibili.api.ServerErrorCode;
import com.hiczp.bilibili.api.passport.entity.LoginResponseEntity; import com.hiczp.bilibili.api.passport.entity.LoginResponseEntity;
import com.hiczp.bilibili.api.passport.entity.RefreshTokenResponseEntity; import com.hiczp.bilibili.api.passport.entity.RefreshTokenResponseEntity;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -31,6 +32,7 @@ public class SecurityHelperTest {
BilibiliSecurityHelper.logout(new BilibiliAPI(), loginResponseEntity.getData().getAccessToken()); BilibiliSecurityHelper.logout(new BilibiliAPI(), loginResponseEntity.getData().getAccessToken());
} }
@Ignore
@Test @Test
public void loginWithWrongUsername() throws Exception { public void loginWithWrongUsername() throws Exception {
LoginResponseEntity loginResponseEntity = BilibiliSecurityHelper.login( LoginResponseEntity loginResponseEntity = BilibiliSecurityHelper.login(