Android Retrofit2 刷新 Oauth 2 token

标签 android oauth-2.0 retrofit2 okhttp

我正在使用 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

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

最佳答案

Disclaimer : Actually I am using Dagger +RxJava + Retrofit but I just wanted to provide an answer to demonstrate logic for future visitors.

Important : If you are making requests from several places your token will refresh multiple times inside TokenAuthenticator class. For example when your activity and your service make requests concurrently. To beat this issue just add synchronized keyword to your TokenAuthenticators authenticate method.

Please make synchronous requests when refreshing your token inside Authenticator because you must block that thread until your request finishes, otherwise your requests will be executed twice with old and new tokens. You can use Schedulers.trampoline() or blockingGet() when refreshing your token to block that thread.

Also inside authenticate method you can check if token is already refreshed by comparing request token with stored token to prevent unnecessary refresh.

And please do not consider using TokenInterceptor because it is edge case and not for everyone, just focus on TokenAuthenticator.

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

enter image description here

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

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

第一步:请引用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);
  }
}

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

关于Android Retrofit2 刷新 Oauth 2 token ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35516626/

相关文章:

c# - https ://<myapp>. azurewebsites.net/.auth/login/aad/callback 是什么意思?

android - Retrofit 2 接口(interface) - 正确的方法?

android - NestedScrollView 的 smoothScrollTo() 行为怪异

python - 无法使用 Yahoo Fantasy Sports API 查找统计数据

android - 在 Android ConstraintLayout 中,工具是什么 :layout_constraintTop_creator attribute for?

android - 使用刷新 token 在访问 token 过期之前刷新访问 token

java - 如何在改造中以多部分形式发送 POJO 形式的嵌套 json 数据?

android - 改造响应给出错误数据

android - "This app contains code that attempts to bypass android' 安全保护的原因"

android - 抽屉导航项目中未找到资源异常