Rxjava2+Retrofit之Token自动刷新

上篇文章主要对Retrofit做了封装,使之使用起来更加方便。在之前的封装中token过期再次刷新token后需要手动调用之前的请求,这种处理方式不够优雅,因此,在原有的基础上,本篇文章将基于上篇文章的封装并优化Token验证机制。使之能够实现过期自动刷新并重新调用请求。

一、初识token验证

1.token是什么?

token意为令牌,通常是由客户端携带IMEI/Mac到服务器,服务器根据客户端的IMEI/Mac生成一段字符串并返回给客户端,并为其设置有效期。以此作为客户端和服务端交互的令牌。客户端每次请求都会携带token到服务器来代替用户名和密码。服务端验证token有效后则返回数据给客户端,否则返回特定的错误码给客户端。客户端根据错误码去做相应的处理。

2.那么为什么引入token机制呢?

主要有以下两个原因:
(1)保证安全性。如果不引入token机制,那么我们每次请求数据都要携带用户名和密码。也就是每次请求数据用户名和密码都会在网络上传输。这样大大增加了安全风险,很容易被黑客截取。因此引入token机制也一定程度上保证了安全性。
(2)减小服务器压力。在引入token机制前,我们需要通过用户名和密码到服务器去验证用户身份是否合法。服务器认证用户名和密码是一个查询操作,如果用户量大,那么就会相应增加服务器的压力。而引入token机制后,服务器就可以将token作为一个用户的唯一标识来验证用户身份是否合法。这样可以大大减少服务器的压力。

3、token机制的验证流程

token的验证流程并非唯一的,至于使用怎样的验证流程可以自行确定。本文中采用OAuth2.0协议实现token验证机制。
主要步骤如下:

  1. 通过用户名和密码登录成功获取token和refreshToken并保存到本地。
  2. token的有效期为2小时,refreshToken的有效期为15天。
  3. 每次网络请求都需要带上token,而不必带上refreshToken。
  4. 如果服务器端判断token过期,则返回对应的错误码,客户端判断错误码后调用刷新token接口,重新获取token和refreshToken并存储。
  5. 如果连续15天未使用app或者用户修改了密码,则refreshToken过期,需要重新登录获取token和refreshToken。

二、 处理token过期异常

有了以上两节的基础,我们就可以来自己实现token机制的验证了。在这里我们使用上篇文章中封装的RxJava和Retrofit来实现token机制。

1.获取token

通过账号密码登录来获取token,登录时我们需要两个参数:用户名username、密码password以及deviceId作为一个唯一id,每次登录成功服务器会返回token和refreshToken。登录请求的实体类LoginRequest如下:

public class LoginRequest {
    private String userId;
    private String password;
    private String deviceId;
    
    public LoginRequest(String userId,String password,String deviceId){
		this.userId = userId;
		this.password = password;
		this.deviceId = deviceId;
	}
}

接下来我们就可以来调用登录接口获取token了。登录成功后我们可将token和refreshToekn存储到本地。以提交表单为例,代码如下:

public void login() {
  
        IdeaApi.getApiService()
                .login(new LoginRequest("123456","123123",getDeviceId()))
                .subscribeOn(Schedulers.io())
                .compose(activity.<BasicResponse<LoginResponse>>bindToLifecycle())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new DefaultObserver<BasicResponse<LoginResponse>>(activity) {
                    @Override
                    public void onSuccess(BasicResponse<LoginResponse> response) {
                        LoginResponse results = response.getResults();
                        ToastUtils.show("登录成功!获取到access token" + results.getAccessToken() + ",可以存储到本地了");
                        /**
                         * 可以将这些数据存储到User中,User存储到本地数据库
                         */
                        SharedPreferencesHelper.put(activity, "access_token", results.getAccessToken());
                        SharedPreferencesHelper.put(activity, "refresh_token", results.getRefreshToken());
                    }
                });
    }

2.异常处理

由于token的有效期较短,因此我们需要经常刷新token来保证token的有效性。在请求网络的时候如果token过期或者无效服务器会给我们返回对应的错误码。我们需要根据状态码来判断token是否失效。如果失效则调用刷新token接口重新获取token。如果refreshToekn也过期那么我们需要重新登录。

现在,我们的需求是要实现token过期后自动刷新,刷新成功后自动调用原来的请求,如果refreshToken也过期,则退出登录。基于此,我们可以联想到RxJava的retryWhen操作符,我们可以通过retryWhen操作符判断token过期并自动刷新。

那么,接下来我们首要任务是如何判断token和refreshToken过期。还记得上篇文章中我们修改GsonResponseBodyConverter类来根据后台响应码来获取data中的数据。显然在此处判断token是否过期是比较合适的。接下来看GsonResponseBodyConverter中的代码:

   @Override
    public Object convert(ResponseBody value) throws IOException {
        try {
            BasicResponse response = (BasicResponse) adapter.fromJson(value.charStream());
            if (response.getCode() == SUCCESS) {
                if (response.getData() == null)
                    throw new ServerNoDataException(0, "");
                return response.getData();
            } else if (response.getCode() == TOKEN_EXPIRED) {
                throw new TokenExpiredException(response.getCode(), response.getMessage());
            } else if (response.getCode() == REFRESH_TOKEN_EXPIRED) {
                throw new RefreshTokenExpiredException(response.getCode(), response.getMessage());
            } else if (response.getCode() != SUCCESS) {
                // 特定 API 的错误,在相应的 DefaultObserver 的 onError 的方法中进行处理
                throw new ServerResponseException(response.getCode(), response.getMessage());
            }
        } finally {
            value.close();
        }
        return null;
    } 

上面代码中我们自定义了几个异常,在判断对应的错误码后抛出对应的异常。此处我们可以着重关心下TokenExpiredException和RefreshTokenExpiredException,分别代表了token过期和refreshToken过期。

三、动态代理,自动刷新access token

1.捕获access token过期异常

注意动态代理ProxyHandler类的invoke方法。该方法即为我们发出的网络请求,正常情况下invoke方法会通过flatMap操作执行method.invoke方法进行正常的网络请求。但若此时access token已过期,则会在在Converer中拦截到access token过期的错误码并抛出TokenInvalidException(该异常是自定义异常)异常,紧接着我们需要在retryWhen中拦截TokenInvalidException异常来通过refresh token刷新access token。代码如下:

public class ProxyHandler implements InvocationHandler {
	// ....省略无关代码
    @Override
    public Object invoke(Object proxy, final Method method, final Object[] args) {
        return Observable.just(true).flatMap((Function<Object, ObservableSource<?>>) o -> {
            try {
                try {
                	 /**
                     * 如果needResetAccessToken为true,则说明本次请求是在成功access token成功刷新后
                     * 自动调用,而此时方法中的access token参数仍然为旧的access token,因此需要
                     * 将这个方法中的access token参数重置为刷新后的access token参数
                     */
                    if (needResetAccessToken) {
                        updateMethodToken(method, args);
                    }
                    return (Observable<?>) method.invoke(mProxyObject, args);
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            return null;
        }).retryWhen(observable -> observable.flatMap((Function<Throwable, ObservableSource<?>>) throwable -> {
            if (throwable instanceof TokenInvalidException) { // 捕捉到token过期异常,则需要刷新token
                return refreshTokenWhenTokenInvalid();
            } else if (throwable instanceof TokenNotExistException) {
                // Token 不存在,执行退出登录的操作。(为了防止多个请求,都出现 Token 不存在的问题,
                // 这里需要取消当前所有的网络请求)
                mGlobalManager.logout();
                return Observable.error(throwable);
            }
            return Observable.error(throwable);
        }));
    }
}

2.刷新access token

在access token成功刷新后将needResetAccessToken置为true。

public class ProxyHandler implements InvocationHandler {

	// ....省略无关代码
	
	/**
	 *  通过refresh token换取新的access token和refresh token并存储到本地,
	 *  刷新成功后将needResetAccessToken置为true,后边请求会根据needResetAccessToken来重置刷新后的token。
	 */
	private Observable<?> refreshTokenWhenTokenInvalid() {
	        synchronized (ProxyHandler.class) {
	            // Have refreshed the token successfully in the valid time.
	            if (new Date().getTime() - tokenChangedTime < REFRESH_TOKEN_VALID_TIME) {
	                mIsTokenNeedRefresh = true;
	                return Observable.just(true);
	            } else {
	                RetrofitService
	                        .getRetrofitBuilder(mBaseUrl)
	                        .build()
	                        .create(CommonService.class)
	                        .refreshToken()
	                        .subscribe(new ResponseObserver<RefreshTokenResponse>() {
	                            @Override
	                            public void onSuccess(RefreshTokenResponse response) {
	                                if (response != null) {
	                                    mGlobalManager.tokenRefresh(response);
	                                    needResetAccessToken= true;
	                                    tokenChangedTime = new Date().getTime();
	                                }
	                            }
	
	                            @Override
	                            public void onError(Throwable e) {
	                                super.onError(e);
	                                mRefreshTokenError = e;
	                            }
	                        });
	                if (mRefreshTokenError != null) {
	                    Observable<Object> error = Observable.error(mRefreshTokenError);
	                    // 这里必须将mRefreshTokenError置空,否则会有问题。
	                    mRefreshTokenError = null;
	                    return error;
	                } else {
	                    return Observable.just(true);
	                }
	            }
	        }
	    }
}

紧接着会继续上面的invoke方法重新发起请求,而此时由于needResetAccessToken为true,则会调用到updateMethodToken来重置请求中旧的access token。代码如下:

 /**
     * Update the access token of the args in the method.
     * <p>
     * access token刷新成功后,再次发起请求需要使用刷新后的access token,
     * 这个方法会拦截本次请求,根据请求方法注入新的access token。
     */
    @SuppressWarnings("unchecked")
    private void updateMethodToken(Method method, Object[] args) {
        String token = (String) SharedPreferencesHelper.get(Utils.getContext(), "token", "");
        if (needResetAccessToken && !TextUtils.isEmpty(token)) {
            Annotation[][] annotationsArray = method.getParameterAnnotations();
            Annotation[] annotations;
            if (annotationsArray != null && annotationsArray.length > 0) {
                for (int i = 0; i < annotationsArray.length; i++) {
                    annotations = annotationsArray[i];
                    for (Annotation annotation : annotations) {
                        if (annotation instanceof FieldMap || annotation instanceof QueryMap) {
                            if (args[i] instanceof Map)
                                ((Map<String, Object>) args[i]).put(ACCESS_TOKEN, token);
                        } else if (annotation instanceof Query) {
                            if (ACCESS_TOKEN.equals(((Query) annotation).value()))
                                args[i] = token;
                        } else if (annotation instanceof Field) {
                            if (ACCESS_TOKEN.equals(((Field) annotation).value()))
                                args[i] = token;
                        } else if (annotation instanceof Part) {
                            if (ACCESS_TOKEN.equals(((Part) annotation).value())) {
                                RequestBody tokenBody = RequestBody.create(MediaType.parse("multipart/form-data"), token);
                                args[i] = tokenBody;
                            }
                        } else if (annotation instanceof Body) {
                            if (args[i] instanceof BaseRequest) {
                                BaseRequest requestData = (BaseRequest) args[i];
                                requestData.setToken(token);
                                args[i] = requestData;
                            }
                        }
                    }
                }
            }
            needResetAccessToken = false;
        }
    }

(一)Rxjava2+Retrofit完美封装
(二)Rxjava2+Retrofit之Token自动刷新
(三)Rxjava2+Retrofit实现文件上传与下载

参考
RxJava+Retrofit实现全局过期token自动刷新Demo篇

源码下载

好库推荐

给大家推荐一下BannerViewPager。这是一个基于ViewPager实现的具有强大功能的无限轮播库。通过BannerViewPager可以实现腾讯视频、QQ音乐、酷狗音乐、支付宝、天猫、淘宝、优酷视频、喜马拉雅、网易云音乐、哔哩哔哩等APP的Banner样式以及指示器样式。

欢迎大家到github关注BannerViewPager

  • 0
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
使用RxJava2 + Retrofit2 + OKHttp进行POST请求,可以按照以下步骤进行: 1. 添加依赖 在项目的build.gradle文件中添加以下依赖: ``` dependencies { implementation 'io.reactivex.rxjava2:rxjava:2.2.19' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.okhttp3:okhttp:4.9.1' } ``` 2. 创建Service接口 创建一个接口,用于定义POST请求的方法。例如: ``` public interface ApiService { @POST("login") Observable<LoginResponse> login(@Body LoginRequest request); } ``` 3. 创建Retrofit对象 在Application类或其他初始化类中,创建Retrofit对象: ``` public class MyApp extends Application { private static ApiService apiService; @Override public void onCreate() { super.onCreate(); // 创建OkHttpClient对象 OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .build(); // 创建Retrofit对象 Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://example.com/api/") .client(client) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build(); apiService = retrofit.create(ApiService.class); } public static ApiService getApiService() { return apiService; } } ``` 4. 发起POST请求 在需要发起POST请求的地方,可以使用以下代码: ``` LoginRequest request = new LoginRequest(); request.setUsername("admin"); request.setPassword("123456"); MyApp.getApiService().login(request) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer<LoginResponse>() { @Override public void onSubscribe(Disposable d) { } @Override public void onNext(LoginResponse response) { // 处理响应数据 } @Override public void onError(Throwable e) { // 处理异常 } @Override public void onComplete() { } }); ``` 上述代码中,我们首先创建了一个LoginRequest对象,用于存储要发送的数据。然后调用MyApp.getApiService().login(request)方法,发起POST请求。在这里,我们使用了RxJava2的Observable对象,将请求结果封装为一个可观察对象。使用subscribeOn(Schedulers.io())指定在IO线程中进行网络请求,使用observeOn(AndroidSchedulers.mainThread())指定在主线程中处理响应。最后通过subscribe方法订阅请求结果,处理响应数据或异常。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值