我正在使用Retrofit处理与服务器API的通信,API用户JSON Web Tokens用于身份验证。令牌不时到期,我正在寻找实现Retrofit Client的最佳方法,该客户端可以在到期时自动刷新令牌。
这是我提出的初步实施,:
/**
* Client implementation that refreshes JSON WebToken automatically if
* the response contains a 401 header, has there may be simultaneous calls to execute method
* the refreshToken is synchronized to avoid multiple login calls.
*/
public class RefreshTokenClient extends OkClient {
private static final int UNAUTHENTICATED = 401;
/**
* Application context
*/
private Application mContext;
public RefreshTokenClient(OkHttpClient client, Application application) {
super(client);
mContext = application;
}
@Override
public Response execute(Request request) throws IOException {
Timber.d("Execute request: " + request.getMethod() + " - " + request.getUrl());
//Make the request and check for 401 header
Response response = super.execute( request );
Timber.d("Headers: "+ request.getHeaders());
//If we received a 401 header, and we have a token, it's most likely that
//the token we have has expired
if(response.getStatus() == UNAUTHENTICATED && hasToken()) {
Timber.d("Received 401 from server awaiting");
//Clear the token
clearToken();
//Gets a new token
refreshToken(request);
//Update token in the request
Timber.d("Make the call again with the new token");
//Makes the call again
return super.execute(rebuildRequest(request));
}
return response;
}
/**
* Rebuilds the request to be executed, overrides the headers with the new token
* @param request
* @return new request to be made
*/
private Request rebuildRequest(Request request){
List<Header> newHeaders = new ArrayList<>();
for( Header h : request.getHeaders() ){
if(!h.getName().equals(Constants.Headers.USER_TOKEN)){
newHeaders.add(h);
}
}
newHeaders.add(new Header(Constants.Headers.USER_TOKEN,getToken()));
newHeaders = Collections.unmodifiableList(newHeaders);
Request r = new Request(
request.getMethod(),
request.getUrl(),
newHeaders,
request.getBody()
);
Timber.d("Request url: "+r.getUrl());
Timber.d("Request new headers: "+r.getHeaders());
return r;
}
/**
* Do we have a token
*/
private boolean hasToken(){
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
return prefs.contains(Constants.TOKEN);
}
/**
* Clear token
*/
private void clearToken(){
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
prefs.edit().remove(Constants.TOKEN).commit();
}
/**
* Saves token is prefs
*/
private void saveToken(String token){
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
prefs.edit().putString(Constants.TOKEN, token).commit();
Timber.d("Saved new token: " + token);
}
/**
* Gets token
*/
private String getToken(){
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
return prefs.getString(Constants.TOKEN,"");
}
/**
* Refreshes the token by making login again,
* //TODO implement refresh token endpoint, instead of making another login call
*/
private synchronized void refreshToken(Request oldRequest) throws IOException{
//We already have a token, it means a refresh call has already been made, get out
if(hasToken()) return;
Timber.d("We are going to refresh token");
//Get credentials
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
String email = prefs.getString(Constants.EMAIL, "");
String password = prefs.getString(Constants.PASSWORD, "");
//Login again
com.app.bubbles.model.pojos.Response<Login> res = ((App) mContext).getApi().login(
new com.app.bubbles.model.pojos.Request<>(credentials)
);
//Save token in prefs
saveToken(res.data.getTokenContainer().getToken());
Timber.d("Token refreshed");
}
}
我不深入了解Retrofit / OkHttpClient的体系结构,但据我所知,execute方法可以从多个线程多次调用,OkClient
仅在Calls
之间共享一个浅的副本完成。
我在synchronized
方法中使用refreshToken()
来避免多个线程进入refreshToken()
并进行多次登录调用,只需要刷新一个线程就应该刷新,其他人将使用更新的令牌。
我还没有认真测试过它,但是我能看到它的工作正常。也许有人已经有了这个问题并且可以分享他的解决方案,或者对于有相同/类似问题的人有帮助。
感谢。
答案 0 :(得分:7)
对于任何发现此问题的人,您应该使用OkHttp拦截器或使用Authenticator API
这是来自Retrofit GitHub页面的示例
public void setup() {
OkHttpClient client = new OkHttpClient();
client.interceptors().add(new TokenInterceptor(tokenManager));
Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.baseUrl("http://localhost")
.build();
}
private static class TokenInterceptor implements Interceptor {
private final TokenManager mTokenManager;
private TokenInterceptor(TokenManager tokenManager) {
mTokenManager = tokenManager;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request initialRequest = chain.request();
Request modifiedRequest = request;
if (mTokenManager.hasToken()) {
modifiedRequest = request.newBuilder()
.addHeader("USER_TOKEN", mTokenManager.getToken())
.build();
}
Response response = chain.proceed(modifiedRequest);
boolean unauthorized = response.code() == 401;
if (unauthorized) {
mTokenManager.clearToken();
String newToken = mTokenManager.refreshToken();
modifiedRequest = request.newBuilder()
.addHeader("USER_TOKEN", mTokenManager.getToken())
.build();
return chain.proceed(modifiedRequest);
}
return response;
}
}
interface TokenManager {
String getToken();
boolean hasToken();
void clearToken();
String refreshToken();
}
如果要在身份验证完成之前阻止请求,可以使用我在答案中执行的相同同步机制,因为拦截器可以在多个线程上并发运行