Android Retrofit2 刷新 Oauth 2 令牌

Posted

技术标签:

【中文标题】Android Retrofit2 刷新 Oauth 2 令牌【英文标题】:Android Retrofit2 Refresh Oauth 2 Token 【发布时间】:2016-06-01 16:40:00 【问题描述】:

我正在使用 RetrofitOkHttp 库。我有一个Authenticator,当我们收到401 响应时,它会对用户进行身份验证。

我的build.gradle是这样的:

compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
compile 'com.squareup.okhttp3:okhttp:3.1.2'

而我的Authenticator是这样的:

public class CustomAuthanticator  implements Authenticator 
@Override
public Request authenticate(Route route, Response response) throws IOException 
    
    //refresh access token
    refreshTokenResult=apiService.refreshUserToken(parameters);
    //this is synchronous retrofit request
    RefreshTokenResult refreshResult = refreshTokenResult.execute().body();
    //check if response equals 400, means empty response
    if(refreshResult != null) 
        // save new access and refresh token
        // then create a new request and new access token as header
        return response.request().newBuilder()
                .header("Authorization", newaccesstoken)
                .build();

     else 
        // we got empty response and we should return null
        // if we don't return null
        // this method will try to make so many requests to get new access token
        return null;
    
                    

这是我的APIService 课程:

public interface APIService 

@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST("token")
public Call<RefreshTokenResult> refreshUserToken(@Header("Accept") String accept, 
    @Header("Content-Type") String contentType, @Field("grant_type") String grantType,
    @Field("client_id") String clientId, @Field("client_secret") String clientSecret, 
    @Field("refresh_token") String refreshToken);

我正在像这样使用Retrofit

CustomAuthanticator customAuthanticator=new CustomAuthanticator();
OkHttpClient okClient = new OkHttpClient.Builder()
        .authenticator(customAuthanticator)
        .build();
Retrofit client = new Retrofit.Builder()
        .baseUrl(getResources().getString(R.string.base_api_url))
        .addConverterFactory(GsonConverterFactory.create(gson))
        .client(okClient)
        .build();
    
//then make retrofit request

所以我的问题是:有时我会得到一个新的访问令牌并继续工作。但有时我会收到一个400 响应,这意味着一个空响应。所以我的旧刷新令牌无效,我无法获得新令牌。通常我们的刷新令牌会在 1 年内到期。所以我怎么能做到这一点。请帮帮我!

【问题讨论】:

【参考方案1】:

免责声明: 实际上我正在使用Dagger +RxJava + Retrofit 但我只是想提供一个答案来为未来的访问者演示逻辑。

重要: 如果您从多个地方发出请求,您的令牌将在 TokenAuthenticator 类中多次刷新。例如,当您的活动和服务同时发出请求时。要解决这个问题,只需将 synchronized 关键字添加到您的 TokenAuthenticators authenticate 方法中即可。

Authenticator 内刷新令牌时请发出同步请求,因为您必须阻塞该线程直到您的请求完成,否则您的请求将使用旧令牌和新令牌执行两次。 刷新令牌时可以使用Schedulers.trampoline()blockingGet() 来阻止该线程。

同样在authenticate 方法中,您可以通过将请求令牌与存储的令牌进行比较来检查令牌是否已经刷新,以防止不必要的刷新。

请不要考虑使用TokenInterceptor,因为它是边缘情况,并不适合所有人,只关注TokenAuthenticator

这是我们正在努力实现的目标:

首先,刷新令牌是大多数应用程序的关键过程。 流程是:如果刷新令牌失败,则注销当前用户并要求重新登录。 (可能在注销用户之前重试几次刷新令牌)

反正我会一步一步解释的:

第 1 步: 请参考singleton pattern,我们将创建一个类来负责返回我们的改造实例。因为如果没有可用的实例它是静态的,它只会创建一次实例,当你调用它时总是返回这个静态实例。这也是单例设计模式的基本定义。

public class RetrofitClient 

private static Retrofit retrofit = null;

private RetrofitClient() 
    // private constructor to prevent access
    // only way to access: Retrofit client = RetrofitClient.getInstance();


public static Retrofit getInstance() 
    if (retrofit == null) 
        // TokenAuthenticator can be singleton too
        TokenAuthenticator tokenAuthenticator = new TokenAuthenticator();

        // !! This interceptor is not required for everyone !!
        // Main purpose of this interceptor is to reduce server calls

        // Our token needs to be refreshed after 10 hours
        // We open our app after 50 hours and try to make a request.
        // Of course token is expired and we will get a 401 response.
        // So this interceptor checks time and refreshes token beforehand.
        // If this fails and I get 401 then my TokenAuthenticator does its job.
        // if my TokenAuthenticator fails too, basically I just logout the user.
        TokenInterceptor tokenInterceptor = new TokenInterceptor();

        OkHttpClient okClient = new OkHttpClient.Builder()
                .authenticator(tokenAuthenticator)
                .addInterceptor(tokenInterceptor)
                .build();

        retrofit = new Retrofit.Builder()
                .baseUrl(base_api_url)
                .client(okClient)
                .build();
    
    return retrofit;
  

第 2 步:在我的 TokenAuthenticator 的 authenticate 方法中:

@Override
public synchronized Request authenticate(Route route, Response response) throws IOException 

    boolean refreshResult = refreshToken();
    if (refreshResult) 
    // refresh token is successful, we saved new token to storage.
    // Get your token from storage and set header
    String newaccesstoken = "your new access token";

    // execute failed request again with new access token
    return response.request().newBuilder()
            .header("Authorization", newaccesstoken)
            .build();

     else 
        // Refresh token failed, you can logout user or retry couple of times
        // Returning null is critical here, it will stop the current request
        // If you do not return null, you will end up in a loop calling refresh
        return null;
    

还有refreshToken 方法,这只是一个您可以创建自己的示例:

public boolean refreshToken() 
    // you can use RxJava with Retrofit and add blockingGet
    // it is up to you how to refresh your token
    RefreshTokenResult result = retrofit.refreshToken();
    int responseCode = result.getResponseCode();

    if(responseCode == 200) 
        // save new token to sharedpreferences, storage etc.
        return true;
     else 
        //cannot refresh
        return false;
     

第 3 步:对于那些想看TokenInterceptor 逻辑的人:

public class TokenInterceptor implements Interceptor 
SharedPreferences prefs;
SharedPreferences.Editor prefsEdit;

@Override
public Response intercept(Chain chain) throws IOException 

    Request newRequest = chain.request();

    // get expire time from shared preferences
    long expireTime = prefs.getLong("expiretime",0);
    Calendar c = Calendar.getInstance();
    Date nowDate = c.getTime();
    c.setTimeInMillis(expireTime);
    Date expireDate = c.getTime();

    int result = nowDate.compareTo(expireDate);
    // when comparing dates -1 means date passed so we need to refresh token
    if(result == -1) 
        //refresh token here , and get new access token
        TokenResponse tokenResponse = refreshToken();

        // Save refreshed token's expire time :
        integer expiresIn = tokenResponse.getExpiresIn();
        Calendar c = Calendar.getInstance();
        c.add(Calendar.SECOND,expiresIn);
        prefsEdit.putLong("expiretime",c.getTimeInMillis());

        String newaccessToken = "new access token";
        newRequest=chain.request().newBuilder()
                .header("Authorization", newaccessToken)
                .build();
    
    return chain.proceed(newRequest);
  

我在活动和后台服务中提出请求。它们都使用相同的改造实例,我可以轻松管理访问令牌。请参考此答案并尝试创建您自己的客户端。如果您仍有问题,请在下方评论,我会尽力提供帮助。

【讨论】:

好答案。我在哪里可以找到TokenInterceptor 类? 收到成功令牌刷新响应后,您必须保存过期时间以便以后使用。所以那部分代码演示了它。 感谢@YasinKaçmaz 提供如此详尽的解释。我只是想知道您将访问令牌存储在您的应用程序中的什么位置 --> 我的意思是哪个是最安全的地方。 @jitenshah 啊,我在 WC 经常想到的那个问题。现在我使用SharedPreferences,但您可以使用Realm 和Encryption 或使用SQLite 加密,还有一些库用加密包装SharedPreferences。但我真的不知道这是否真的需要并且没有测试性能障碍。无论如何,也许谷歌这个:“sharedpreferences security”,如果你仍然感兴趣,我们总是可以争论这个。 这是迄今为止这类问题的最佳答案,其他的只是带有一堆未解决问题的伪代码。【参考方案2】:

在您的 ApiClient.java 类中:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new AuthorizationInterceptor(context))
                .build();

在改造包中添加 TokenManager.java

package co.abc.retrofit;

/**
 * Created by ravindrashekhawat on 17/03/17.
 */

public interface TokenManager 
    String getToken();
    boolean hasToken();
    void clearToken();
    String refreshToken();

在您的包中添加名称为 AuthorizationInterceptor.java

的拦截器类
package co.smsmagic.retrofit;

import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;

import com.google.gson.Gson;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;

import co.abc.models.RefreshTokenResponseModel;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Retrofit;
import retrofit2.http.Header;

import static co.abc.utils.abcConstants.ACCESS_TOKEN;
import static co.abc.utils.abcConstants.BASE_URL;
import static co.abc.utils.abcConstants.GCM_TOKEN;
import static co.abc.utils.abcConstants.JWT_TOKEN_PREFIX;
import static co.abc.utils.abcConstants.REFRESH_TOKEN;

/**
 * Created by ravindrashekhawat on 21/03/17.
 */

public class AuthorizationInterceptor implements Interceptor 
    private static Retrofit retrofit = null;
    private static String deviceToken;
    private static String accessToken;
    private static String refreshToken;
    private static TokenManager tokenManager;
    private static Context mContext;

    public AuthorizationInterceptor(Context context) 
        this.mContext = context;
    

    @Override
    public Response intercept(Chain chain) throws IOException 
        Request request = chain.request();
        Request modifiedRequest = null;

        tokenManager = new TokenManager() 
            final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);

            @Override
            public String getToken() 

                accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
                return accessToken;
            

            @Override
            public boolean hasToken() 
                accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
                if (accessToken != null && !accessToken.equals("")) 
                    return true;
                
                return false;
            

            @Override
            public void clearToken() 
                sharedPreferences.edit().putString(ACCESS_TOKEN, "").apply();
            

            @Override
            public String refreshToken() 
                final String accessToken = null;

                RequestBody reqbody = RequestBody.create(null, new byte[0]);
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                        .url(BASE_URL + "refresh")
                        .method("POST", reqbody)
                        .addHeader("Authorization", JWT_TOKEN_PREFIX + refreshToken)
                        .build();

                try 
                    Response response = client.newCall(request).execute();
                    if ((response.code()) == 200) 
                        // Get response
                        String jsonData = response.body().string();

                        Gson gson = new Gson();
                        RefreshTokenResponseModel refreshTokenResponseModel = gson.fromJson(jsonData, RefreshTokenResponseModel.class);
                        if (refreshTokenResponseModel.getRespCode().equals("1")) 
                            sharedPreferences.edit().putString(ACCESS_TOKEN, refreshTokenResponseModel.getResponse()).apply();
                            return refreshTokenResponseModel.getResponse();
                        

                    
                 catch (IOException e) 
                    e.printStackTrace();
                
                return accessToken;
            
        ;

        final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
        deviceToken = sharedPreferences.getString(GCM_TOKEN, "");
        accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
        refreshToken = sharedPreferences.getString(REFRESH_TOKEN, "");

        Response response = chain.proceed(request);
        boolean unauthorized =false;
        if(response.code() == 401 || response.code() == 422)
            unauthorized=true;
        

        if (unauthorized) 
            tokenManager.clearToken();
            tokenManager.refreshToken();
            accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
            if(accessToken!=null)
                modifiedRequest = request.newBuilder()
                        .addHeader("Authorization", JWT_TOKEN_PREFIX + tokenManager.getToken())
                        .build();
                return chain.proceed(modifiedRequest);
            
        
        return response;
    

注意:这是我提供的刷新令牌的工作代码,请保持冷静,只是为了更改一些常量,但它会完美运行。试着理解逻辑。

在底部有再次调用相同请求的逻辑

 if(accessToken!=null)
                modifiedRequest = request.newBuilder()
                        .addHeader("Authorization", JWT_TOKEN_PREFIX + tokenManager.getToken())
                        .build();
                return chain.proceed(modifiedRequest);
  

【讨论】:

那是 401 用于刷新令牌失败而不是 400 。但是我使用 TokenManager r 界面并没有说你的方法有任何问题,也总是有很多方法可以实现相同的解决方案。它可以帮助某人 是的,你的方法很好。但是您必须尝试执行您的请求以查看它是否返回任何 401 响应(未经授权)。您可以将此逻辑移至 OkHttp 的 Authenticator,因为它执行相同的操作,并且您可以在另一个拦截器中检查令牌时间。如果时间到期,基本上你可以刷新你的令牌而不会得到任何 401 响应。

以上是关于Android Retrofit2 刷新 Oauth 2 令牌的主要内容,如果未能解决你的问题,请参考以下文章

Retrofit2缓存实现

retrofit2.HttpException:Android 中的 HTTP 302

Retrofit2.0 android.os.NetworkOnMainThreadException解决方法

Android Studio - 使用 retrofit2 从 restdb 获取信息

Android Retrofit2中Part和PartMap的区别

Android 网络框架 Retrofit2.0介绍使用和封装