项目中验证用户会采用实时变更token的方式,一个refreshToken 五分钟过期一次,一个accessToken 一周失效。refreshToken过期需要刷新替换,accessToken需要过期退出登录。开发中会遇到场景:当前refreshToken已经过期,存在异步几个接口同时持有过期的refreshToken请求。这样的结果可想而知!
项目使用的是retrofit,结合retrofit 的动态代理设计模式,可以引以为用:
- 1.动态代理替换service抽象接口
/**
* 接口service
*
* @param service 具体业务service
* @param <T> 泛型
* @return service 实例
*/
public static <T> T getService(Class<T> service) {
return createProxy(service);
}
private static <S> S createProxy(Class<S> serviceClass) {
S s = getInstance().retrofit.create(serviceClass);
return (S) Proxy.newProxyInstance(serviceClass.getClassLoader(), new Class<?>[]{serviceClass}, new ProxyHandler(s));
}
下面是ProxyHandler 类,返回请求对象观察者:
/**
* @author 桂雁彬
* @date 2019-12-18.
* GitHub:
* description:
*/
public class ProxyHandler implements java.lang.reflect.InvocationHandler {
private final Object target;
private boolean mIsTokenNeedRefresh = true;
private final static String TOKEN = "token";
ProxyHandler(@NonNull final Object target) {
this.target = target;
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
Object r = method.invoke(target, args);
if (!(r instanceof Observable)) {
return r;
}
return Observable.just(true)
.flatMap((Function<Object, ObservableSource<?>>) o -> (Observable<?>) method.invoke(target, args)).retryWhen(new RetryWithDelay(2, 1000));
}
}
由invoke 的返回可看具体操作类RetryWithDelay
public class RetryWithDelay implements Function<Observable<? extends Throwable>, Observable<?>> {
private final int maxRetries;
private final int retryDelayMillis;
private long currentRefreshTime;
private static final long REFRESH_TOKEN_TIME = 200;
private boolean oneTimeRefresh;
public RetryWithDelay(int maxRetries, int retryDelayMillis) {
this.maxRetries = maxRetries;
this.retryDelayMillis = retryDelayMillis;
}
@Override
public Observable<?> apply(Observable<? extends Throwable> observable) {
return observable.flatMap(new Function<Throwable, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(Throwable throwable) throws Exception {
LogUtils.d("TtSy", "throwable:" + throwable);
LogUtils.d("==refreshTpoken", "response:" + "onUnAuth" + " thread:" + Thread.currentThread());
if (throwable instanceof ApiException) {
ApiException apiException = (ApiException) throwable;
if (apiException.getCode() == 20001) { //token 过期 直接退出登录
// apiCode = 16 游客token过期
if (!oneTimeRefresh){
EventBus.getDefault().post(new BaseLoginOutEvent());
oneTimeRefresh=true;
}
}else if (apiException.getCode() == 20002) { //持续刷新token
return onUnAuth(throwable);
}
} else if (throwable instanceof HttpException) {
HttpException httpException = (HttpException) throwable;
if (httpException.code() == 401) {
if (!oneTimeRefresh){
EventBus.getDefault().post(new BaseLoginOutEvent());
oneTimeRefresh=true;
}
}
}
return Observable.error(throwable);
}
});
}
private Observable<?> onUnAuth(Throwable throwable) {
synchronized (ProxyHandler.class) {
// 防止多次调用
if (System.currentTimeMillis() - currentRefreshTime < REFRESH_TOKEN_TIME) {
return Observable.just(true);
}
return Observable.intervalRange(1, maxRetries, 0, retryDelayMillis, TimeUnit.MILLISECONDS)
.flatMap(new Function<Long, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(Long aLong) throws Exception {
LogUtils.d("==refreshTpoken", "response:" + "onUnAuth" + " thread:" + Thread.currentThread());
if (aLong == maxRetries) {
return Observable.error(new ApiException("未知错误", -1));
}
return refreshToken();
}
});
}
}
private Observable<?> refreshToken() {
synchronized (ProxyHandler.class) {
Map<String, Object> map = new HashMap<>(2);
return RetrofitHelp.getService(RefreshTokenApiService.class)
.refreshToken(map)
.flatMap((Function<ResultEntity<RefreshToken>, ObservableSource<?>>) refreshTokenResultEntity -> {
LogUtils.d("==refreshTpoken", "response:" + refreshTokenResultEntity + " thread:" + Thread.currentThread());
RefreshToken data = refreshTokenResultEntity.getData();
if (data != null && !TextUtils.isEmpty(data.getToken())) {
putToken(data.getToken());
currentRefreshTime = System.currentTimeMillis();
return Observable.just(true);
}
return Observable.error(new ApiException("未知错误", -1));
});
}
}
/**
* 保存本地token
*
* @param token
*/
public void putToken(String token) {
PSP.getInstance().remove(FinalKey.USER_TOKEN);
PSP.getInstance().put(FinalKey.USER_TOKEN, token);
}
}
上面的代码主要的思路是对应服务协议token失效会返回对应的code,当前 code 20002时去同步刷新token并返回最终的Observable,这样其他请求并不会同时异步持有过期token,会在本次刷新完后持有最新token刷新。
注:上面的
if (throwable instanceof ApiException) {
ApiException apiException = (ApiException) throwable;
if (apiException.getCode() == 20001) { //token 过期 直接退出登录
// apiCode = 16 游客token过期
if (!oneTimeRefresh){
EventBus.getDefault().post(new BaseLoginOutEvent());
oneTimeRefresh=true;
}
}else if (apiException.getCode() == 20002) { //持续刷新token
return onUnAuth(throwable);
}
是在网络请求根据服务端返回对应的code,如果不是请求成功码会抛出异常,由rxJava 处理最终拿到回调throwable。
可以在自定义Converter 中实现:
public class CustomGsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
private final Gson gson;
private final TypeAdapter<T> adapter;
CustomGsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
this.gson = gson;
this.adapter = adapter;
}
@Override
public T convert(@NonNull ResponseBody value) throws IOException {
String string = value.string();
LogUtils.d("==temp==",string);
ResultEntity baseNewEntity = gson.fromJson(string, ResultEntity.class);
if (baseNewEntity == null) {
value.close();
throw new ApiException("数据错误", -1);
}
if (!baseNewEntity.isSuccess()) {
value.close();
throw new ApiException(baseNewEntity.getMsg(), baseNewEntity.getCode());
}
MediaType contentType = value.contentType();
Charset charset = contentType != null ? contentType.charset(UTF_8) : UTF_8;
InputStream inputStream = new ByteArrayInputStream(string.getBytes());
Reader reader = new InputStreamReader(inputStream, charset);
JsonReader jsonReader = gson.newJsonReader(reader);
try {
return adapter.read(jsonReader);
} finally {
value.close();
}
}
private static final Charset UTF_8 = Charset.forName("UTF-8");
}