sat*_*sat 31 java android http retrofit okhttp
我同时加载了一个ViewPager和三个webservice调用ViewPager.
当第一个返回401时,Authenticator调用并刷新内部令牌Authenticator,但剩余的2个请求已经发送到具有旧刷新令牌的服务器,并且失败,498在Interceptor中捕获并且应用程序被注销.
这不是我期望的理想行为.我想将第二个和第三个请求保留在队列中,当刷新令牌时,重试排队的请求.
目前,我有一个变量来指示令牌刷新是否正在进行中Authenticator,在这种情况下,我取消了所有后续请求Interceptor,用户必须手动刷新页面,否则我可以注销用户并强制用户登录.
使用okhttp 3.x for Android的上述问题有什么好的解决方案或架构?
编辑:我想解决的问题一般,我不想对我的电话进行排序.即等待一个调用完成并刷新令牌,然后仅在活动和片段级别上发送其余请求.
代码被要求.这是一个标准代码Authenticator:
public class CustomAuthenticator implements Authenticator {
@Inject AccountManager accountManager;
@Inject @AccountType String accountType;
@Inject @AuthTokenType String authTokenType;
@Inject
public ApiAuthenticator(@ForApplication Context context) {
}
@Override
public Request authenticate(Route route, Response response) throws IOException {
// Invaidate authToken
String accessToken = accountManager.peekAuthToken(account, authTokenType);
if (accessToken != null) {
accountManager.invalidateAuthToken(accountType, accessToken);
}
try {
// Get new refresh token. This invokes custom AccountAuthenticator which makes a call to get new refresh token.
accessToken = accountManager.blockingGetAuthToken(account, authTokenType, false);
if (accessToken != null) {
Request.Builder requestBuilder = response.request().newBuilder();
// Add headers with new refreshToken
return requestBuilder.build();
} catch (Throwable t) {
Timber.e(t, t.getLocalizedMessage());
}
}
return null;
}
}
Run Code Online (Sandbox Code Playgroud)
一些类似的问题: OkHttp和Retrofit,用并发请求刷新令牌
Dav*_*jak 12
值得注意的是accountManager.blockingGetAuthToken,除了拦截器之外,还可以在其他地方调用(或非阻塞版本).因此,防止此问题发生的正确位置将在验证器内.
我们希望确保需要访问令牌的第一个线程将检索它,并且可能的其他线程应该只注册在第一个线程完成检索令牌时调用的回调.
好消息是,AbstractAccountAuthenticator已经有了一种提供异步结果的方法,即AccountAuthenticatorResponse你可以调用onResult或者调用onError.
以下示例包含3个块.
在第一个是关于如何确保只有一个线程获取访问令牌,而其他线程只是登记他们的response回调.
在第二部分仅仅是一个虚拟空的结果束.在这里,您可以加载令牌,可能会刷新它等.
在第三部分是,一旦你有你的结果(或错误),你做什么.您必须确保为可能已注册的每个其他线程调用响应.
boolean fetchingToken;
List<AccountAuthenticatorResponse> queue = null;
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
synchronized (this) {
if (fetchingToken) {
// another thread is already working on it, register for callback
List<AccountAuthenticatorResponse> q = queue;
if (q == null) {
q = new ArrayList<>();
queue = q;
}
q.add(response);
// we return null, the result will be sent with the `response`
return null;
}
// we have to fetch the token, and return the result other threads
fetchingToken = true;
}
// load access token, refresh with refresh token, whatever
// ... todo ...
Bundle result = Bundle.EMPTY;
// loop to make sure we don't drop any responses
for ( ; ; ) {
List<AccountAuthenticatorResponse> q;
synchronized (this) {
// get list with responses waiting for result
q = queue;
if (q == null) {
fetchingToken = false;
// we're done, nobody is waiting for a response, return
return null;
}
queue = null;
}
// inform other threads about the result
for (AccountAuthenticatorResponse r : q) {
r.onResult(result); // return result
}
// repeat for the case another thread registered for callback
// while we were busy calling others
}
}
Run Code Online (Sandbox Code Playgroud)
只需确保null在使用时返回所有路径response.
您显然可以使用其他方法来同步这些代码块,例如@matrix在另一个响应中所示的原子.我利用了synchronized,因为我认为这是最容易掌握的实现,因为这是一个很好的问题,每个人都应该这样做;)
上面的示例是此处描述的发射器循环的改编版本,其中详细介绍了并发性.如果您对RxJava如何在幕后工作感兴趣,这个博客是一个很好的来源.
你可以这样做:
将它们添加为数据成员:
// these two static variables serve for the pattern to refresh a token
private final static ConditionVariable LOCK = new ConditionVariable(true);
private static final AtomicBoolean mIsRefreshing = new AtomicBoolean(false);
Run Code Online (Sandbox Code Playgroud)
然后在拦截方法上:
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Request request = chain.request();
// 1. sign this request
....
// 2. proceed with the request
Response response = chain.proceed(request);
// 3. check the response: have we got a 401?
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
if (!TextUtils.isEmpty(token)) {
/*
* Because we send out multiple HTTP requests in parallel, they might all list a 401 at the same time.
* Only one of them should refresh the token, because otherwise we'd refresh the same token multiple times
* and that is bad. Therefore we have these two static objects, a ConditionVariable and a boolean. The
* first thread that gets here closes the ConditionVariable and changes the boolean flag.
*/
if (mIsRefreshing.compareAndSet(false, true)) {
LOCK.close();
/* we're the first here. let's refresh this token.
* it looks like our token isn't valid anymore.
* REFRESH the actual token here
*/
LOCK.open();
mIsRefreshing.set(false);
} else {
// Another thread is refreshing the token for us, let's wait for it.
boolean conditionOpened = LOCK.block(REFRESH_WAIT_TIMEOUT);
// If the next check is false, it means that the timeout expired, that is - the refresh
// stuff has failed.
if (conditionOpened) {
// another thread has refreshed this for us! thanks!
// sign the request with the new token and proceed
// return the outcome of the newly signed request
response = chain.proceed(newRequest);
}
}
}
}
// check if still unauthorized (i.e. refresh failed)
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
... // clean your access token and prompt for request again.
}
// returning the response to the original request
return response;
}
Run Code Online (Sandbox Code Playgroud)
通过这种方式,您只需发送1个请求来刷新令牌,然后为每个其他请求刷新令牌.
您可以尝试使用此应用程序级拦截器
private class HttpInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
//Build new request
Request.Builder builder = request.newBuilder();
builder.header("Accept", "application/json"); //if necessary, say to consume JSON
String token = settings.getAccessToken(); //save token of this request for future
setAuthHeader(builder, token); //write current token to request
request = builder.build(); //overwrite old request
Response response = chain.proceed(request); //perform request, here original request will be executed
if (response.code() == 401) { //if unauthorized
synchronized (httpClient) { //perform all 401 in sync blocks, to avoid multiply token updates
String currentToken = settings.getAccessToken(); //get currently stored token
if(currentToken != null && currentToken.equals(token)) { //compare current token with token that was stored before, if it was not updated - do update
int code = refreshToken() / 100; //refresh token
if(code != 2) { //if refresh token failed for some reason
if(code == 4) //only if response is 400, 500 might mean that token was not updated
logout(); //go to login screen
return response; //if token refresh failed - show error to user
}
}
if(settings.getAccessToken() != null) { //retry requires new auth token,
setAuthHeader(builder, settings.getAccessToken()); //set auth token to updated
request = builder.build();
return chain.proceed(request); //repeat request with new token
}
}
}
return response;
}
private void setAuthHeader(Request.Builder builder, String token) {
if (token != null) //Add Auth token to each request if authorized
builder.header("Authorization", String.format("Bearer %s", token));
}
private int refreshToken() {
//Refresh token, synchronously, save it, and return result code
//you might use retrofit here
}
private int logout() {
//logout your user
}
}
Run Code Online (Sandbox Code Playgroud)
您可以将这样的拦截器设置为 okHttp 实例
Gson gson = new GsonBuilder().create();
OkHttpClient httpClient = new OkHttpClient();
httpClient.interceptors().add(new HttpInterceptor());
final RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(BuildConfig.REST_SERVICE_URL)
.setClient(new OkClient(httpClient))
.setConverter(new GsonConverter(gson))
.setLogLevel(RestAdapter.LogLevel.BASIC)
.build();
remoteService = restAdapter.create(RemoteService.class);
Run Code Online (Sandbox Code Playgroud)
希望这可以帮助!!!!
| 归档时间: |
|
| 查看次数: |
5564 次 |
| 最近记录: |