会话过期后token刷新,重新请求接口(订阅发布模式)

需求
在一个页面内,当请求失败并且返回 302 后,判断是接口过期还是登录过期,如果是接口过期,则去请求新的token,然后拿新的token去再次发起请求.

在这里插入图片描述

思路
当初,想了一个黑科技(为了偷懒),就是拿到新的token后,直接强制刷新页面,这样一个页面内的接口就自动刷新啦~(方便是方便,用户体验却不好)
目前,想到了重新请求接口时,可以配合订阅发布模式来提高用户体验
响应拦截
首先我们发起一个请求 axios({url:’/test’,data:xxx}).then(res=>{})

拦截到302后,我们进入到刷新token逻辑

响应拦截代码

axios.interceptors.response.use(
    function (response) { 
        if (response.status == 200) { 
            return response;
        }
    },
    (err) => {
        //刷新token
        let res = err.response || {}; 
        if (res.data.meta?.statusCode == 302) {
            return refeshToken(res);
        } else {  
            return err;
        }
    }
);
复制代码

我们后台的数据格式是根据statusCode来判断过期(你们可以根据自己的实际情况判断),接着进入refrshToken方法~

刷新token方法

//避免其他接口同时请求(只请求一次token接口)
let isRefreshToken = false;
const refeshToken = (response) => {
   if (!isRefreshToken) {
            isRefreshToken = true;
            axios({
                //获取新token接口
                url: `/api/refreshToken`,
            })
                .then((res) => {
                    const { data = '', meta = {} } = res.data;
                    if (meta.statusCode === 200) {
                        isRefreshToken = false; 
                        //发布 消息
                        retryOldRequest.trigger(data);
                    } else { 
                        history.push('/user/login');
                    }
                })
                .catch((err) => { 
                    history.push('/user/login');
                });
        }
        //收集订阅者 并把成功后的数据返回原接口
        return retryOldRequest.listen(response);
};
复制代码

看到这,有的小伙伴就有点奇怪retryOldRequest这个又是什么?没错,这就是我们男二 订阅发布模式队列。

订阅发布模式
把失败的接口当订阅者,成功拿到新的token后再发布(重新请求接口)。

以下便是订阅发布模式代码

const retryOldRequest = {
    //维护失败请求的response
    requestQuery: [],

    //添加订阅者
    listen(response) {
        return new Promise((resolve) => {
            this.requestQuery.push((newToken) => { 
                let config = response.config || {};
                //Authorization是传给后台的身份令牌
                config.headers['Authorization'] = newToken;
                resolve(axios(config));
            });
        });
    },

    //发布消息
    trigger(newToken) {
        this.requestQuery.forEach((fn) => {
            fn(newToken);
        });
        this.requestQuery = [];
    },
};
复制代码

大家可以先不用关注订阅者的逻辑,只需要知道订阅者是每次请求失败后的接口(reponse)就好了。

每次进入refeshToken方法,我们失败的接口都会触发retryOldRequest.listen去订阅,而我们的requestQuery则是保存这些订阅者的队列。

注意:我们订阅者队列requestQuery是保存待发布的方法。而在成功获取新token后,retryOldRequest.trigger就会去发布这些消息(新token)给订阅者(触发订阅队列的方法)。

而订阅者(response)里面有config配置,我们拿到新的token后(发布后),修改config里面的请求头Autorzation.而借助Promise我们可以更好的拿到新token请求回来的接口数据,一旦请求到数据,我们可以原封不动的返回给原来的接口/test了(因为我们在响应拦截那里返回的是refreshToken,而refreshToken又返回的是订阅者retryOldRequest.listen返回的数据,而Listiner又返回Promise的数据,Promise又在成功请求后resolve出去)。

看到这,小伙伴们是不是觉得有点绕了~

而在真实开发中,我们的逻辑还含有登录过期(与请求过期区分开来)。我们是根据 当前时间 - 过去时间 < expiresTime(epiresTime:登录后返回的有效时间)来判断是请求过期还是登录过期的。 以下是完整逻辑

在这里插入图片描述

以下是完整代码

const retryOldRequest = {
    //维护失败请求的response
    requestQuery: [],

    //添加订阅者
    listen(response) {
        return new Promise((resolve) => {
            this.requestQuery.push((newToken) => { 
                let config = response.config || {};
                config.headers['Authorization'] = newToken;
                resolve(axios(config));
            });
        });
    },

    //发布消息
    trigger(newToken) {
        this.requestQuery.forEach((fn) => {
            fn(newToken);
        });
        this.requestQuery = [];
    },
};
/**
 * sessionExpiredTips
 * 会话过期:
 * 刷新token失败,得重新登录
 * 用户未授权,页面跳转到登录页面 
 * 接口过期 => 刷新token
 * 登录过期 => 重新登录
 * expiresTime => 在本业务中返回18000ms == 5h
 * ****/

//避免其他接口同时请求
let isRefreshToken = false;
let timer = null;
const refeshToken = (response) => {
    //登录后拿到的有效期
    let userExpir = localStorage.getItem('expiresTime');
    //当前时间
    let nowTime = Math.floor(new Date().getTime() / 1000);
    //最后请求的时间
    let lastResTime = localStorage.getItem('lastResponseTime') || nowTime;
    //登录后保存到本地的token
    let token = localStorage.getItem('token');

    if (token && nowTime - lastResTime < userExpir) {
        if (!isRefreshToken) {
            isRefreshToken = true;
            axios({
                url: `/api/refreshToken`,
            })
                .then((res) => {
                    const { data = '', meta = {} } = res.data;
                    isRefreshToken = false;
                    if (meta.statusCode === 200) {
                        localStorage.setItem('token', data);
                        localStorage.setItem('lastResponseTime', Math.floor(new Date().getTime() / 1000)
                        );
                        //发布 消息
                        retryOldRequest.trigger(data);
                    } else {
                       //去登录
                    }
                })
                .catch((err) => {
                    isRefreshToken = false;
                   //去登录
                });
        }
        //收集订阅者 并把成功后的数据返回原接口
        return retryOldRequest.listen(response);
    } else {
        //节流:避免重复运行
       //去登录
    }
};

// http response 响应拦截
axios.interceptors.response.use(
    function (response) { 
        if (response.status == 200) {
            //记录最后操作时间
           localStorage.setItem('lastResponseTime', Math.floor(new Date().getTime() / 1000));
            return response;
        }
    },
    (err) => { 
        let res = err.response || {}; 
        if (res.data.meta?.statusCode == 302) {
            return refeshToken(res);
        } else {
            // 302 报的错误; 
            return err;
        }
    }
);

复制代码

以上便是我们这边的业务,如果写的不好请大佬多担待~~

最后
如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163相互学习,我们会有专业的技术答疑解惑

如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star:http://github.crmeb.net/u/defu不胜感激 !

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,以下是一个可能的代码实现,供您参考: 首先,我们需要定义一个TokenInterceptor拦截器类,用于在每次请求接口时检查Token是否过期: ```java public class TokenInterceptor implements Interceptor { private static final String TOKEN_KEY = "token"; private static final String AUTHORIZATION_HEADER = "Authorization"; private SharedPreferences mSharedPreferences; private ApiService mApiService; public TokenInterceptor(Context context, ApiService apiService) { mSharedPreferences = context.getSharedPreferences("my_prefs", Context.MODE_PRIVATE); mApiService = apiService; } @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); String token = mSharedPreferences.getString(TOKEN_KEY, ""); if (!TextUtils.isEmpty(token)) { request = request.newBuilder() .addHeader(AUTHORIZATION_HEADER, "Bearer " + token) .build(); } Response response = chain.proceed(request); if (response.code() == 401) { // Token过期重新请求Token并重试请求 String newToken = getNewToken(); if (!TextUtils.isEmpty(newToken)) { // 更新Token并重试请求 mSharedPreferences.edit().putString(TOKEN_KEY, newToken).apply(); Request newRequest = request.newBuilder() .addHeader(AUTHORIZATION_HEADER, "Bearer " + newToken) .build(); response = chain.proceed(newRequest); } } return response; } private String getNewToken() { // 发送获取新Token请求,返回新Token Call<TokenResponse> call = mApiService.refreshToken(); try { Response<TokenResponse> response = call.execute(); if (response.isSuccessful()) { TokenResponse tokenResponse = response.body(); return tokenResponse.getToken(); } } catch (IOException e) { e.printStackTrace(); } return null; } } ``` 然后,在创建Retrofit实例时,添加上述拦截器: ```java OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new TokenInterceptor(context, apiService)) .build(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); ``` 其中,apiService是一个定义了刷新Token接口的Retrofit接口。 最后,在请求A接口时,如果遇到Token过期的情况,TokenInterceptor会自动发送请求获取新Token,并重新发送A接口请求: ```java Call<ResponseA> call = apiService.getAData(); call.enqueue(new Callback<ResponseA>() { @Override public void onResponse(Call<ResponseA> call, Response<ResponseA> response) { if (response.isSuccessful()) { // 请求成功 ResponseA responseA = response.body(); // 处理返回的数据 } else { // 请求失败 // 这里不需要处理401 Unauthorized错误,TokenInterceptor已经处理过了 } } @Override public void onFailure(Call<ResponseA> call, Throwable t) { // 请求失败 } }); ``` 以上就是一个简单的android token过期刷新重新请求A接口的代码实现,仅供参考。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CRMEB定制开发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值