Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] 如何像Axios一样无痛刷新Token #92

Closed
Lin-Van opened this issue May 22, 2023 · 23 comments
Closed

[Feature Request] 如何像Axios一样无痛刷新Token #92

Lin-Van opened this issue May 22, 2023 · 23 comments

Comments

@Lin-Van
Copy link

Lin-Van commented May 22, 2023

你在什么情况下,需要这个功能解决什么问题?

刷新token之后发现原来的请求Token还是旧的,得第二次发送请求才能生效,如何拦截后就新的Token立即生效,代码如下:

beforeRequest: (method) => {
            if (getCache(TOKEN_KEY) == null) { //Token过期
                new Promise(resolve => {
                    const token = authStore.getRefreshToken;
                    const { send: sendRefreshToken } = useRequest(refreshToken, { immediate: false });
                    sendRefreshToken({ refreshToken: token })
                        .then((res) => {
                            if (res.success) {
                                authStore.setToken(res);
                                method.config.headers = assign(method.config.headers, authStore.getAuthorization);
                                resolve(method);
                            }
                        });
                });
            } 
    },

你期望的 API 是什么样子的?

beforeRequest中刷新Token,method.config.headers中Token可以立即更新

@JOU-amjs
Copy link
Contributor

可以参考这个issue #56

@Lin-Van
Copy link
Author

Lin-Van commented May 22, 2023

可以参考这个issue #56

这个我看过了,onSuccess里面请求相当于发了两次请求,我希望原请求的只发送一次。
beforeRequest刷新Token的时候,要是method.config能刷新,那就只发送一次请求。

@JOU-amjs
Copy link
Contributor

JOU-amjs commented May 22, 2023

我的理解是,你希望在beforeRequest中判断token是否过期,过期的话就通过refreshToken刷新token,并继续发送请求。
如果是这样的话,你只需要在beforeRequest中返回这个promise对象就好了,它会等待这个promise resolve后再继续发送请求。

beforeRequest: (method) => {
        if (getCache(TOKEN_KEY) == null) { //Token过期
            return new Promise(resolve => {
                const token = authStore.getRefreshToken;
                const { send: sendRefreshToken } = useRequest(refreshToken, { immediate: false });
                sendRefreshToken({ refreshToken: token })
                    .then((res) => {
                        if (res.success) {
                            authStore.setToken(res);
                            method.config.headers = assign(method.config.headers, authStore.getAuthorization);
                            resolve(method);
                        }
                    });
            });
        } 
    },

@martinadamsdev
Copy link

@Lin-Van 不知道你的具体需求。我们这边验证 token 和 refresh token 后端都是做的两个接口,根本不存在你只发送一次请求。

@Lin-Van
Copy link
Author

Lin-Van commented May 23, 2023

我的理解是,你希望在beforeRequest中判断token是否过期,过期的话就通过refreshToken刷新token,并继续发送请求。 如果是这样的话,你只需要在beforeRequest中返回这个promise对象就好了,它会等待这个promise resolve后再继续发送请求。

beforeRequest: (method) => {
        if (getCache(TOKEN_KEY) == null) { //Token过期
            return new Promise(resolve => {
                const token = authStore.getRefreshToken;
                const { send: sendRefreshToken } = useRequest(refreshToken, { immediate: false });
                sendRefreshToken({ refreshToken: token })
                    .then((res) => {
                        if (res.success) {
                            authStore.setToken(res);
                            method.config.headers = assign(method.config.headers, authStore.getAuthorization);
                            resolve(method);
                        }
                    });
            });
        } 
    },

是滴 我的需求是正如你所说的那样 谢谢你给的灵感 我稍微修改了下 最后改成这样

beforeRequest: (method) => {
        if (getCache(TOKEN_KEY) == null) { //Token过期
                const token = authStore.getRefreshToken;
                const { send: sendRefreshToken } = useRequest(refreshToken, { immediate: false });
                return sendRefreshToken({ refreshToken: token })
                    .then((res) => {
                        if (res.success) {
                            authStore.setToken(res);
                            method.config.headers = assign(method.config.headers, authStore.getAuthorization);
                            resolve(method);
                        }
                    });
        } 
    },

@Lin-Van
Copy link
Author

Lin-Van commented May 23, 2023

@Lin-Van 不知道你的具体需求。我们这边验证 token 和 refresh token 后端都是做的两个接口,根本不存在你只发送一次请求。

也是两个接口,只是前端是在请求的时候拦截,刷新Token,然后继续之前的的请求,算一次请求。
之前的问题,里面是响应拦截,判断Token失效刷新Token,再重新发送请求,相当于发送两次请求。
具体可以看我上面的回复。

@JOU-amjs
Copy link
Contributor

JOU-amjs commented May 23, 2023

@Lin-Van 对了,运行起来应该是没问题的,只是代码的使用规范可能还可以再改善一下,use hook其实建议是在组件中使用的,因为其内部调用了生命周期钩子,而且这边也用不着如loadingerror等响应式状态。

如果在组件外发起请求的话,可以直接使用method实例就可以了,因此这段代码可以简化成这样:

beforeRequest: (method) => {
    if (getCache(TOKEN_KEY) == null) { //Token过期
            const token = authStore.getRefreshToken;
            return refreshToken({ refreshToken: token })
                .send()
                .then((res) => {
                    if (res.success) {
                        authStore.setToken(res);
                        method.config.headers = assign(method.config.headers, authStore.getAuthorization);
                        resolve(method);
                    }
                });
    } 
},

@Lin-Van Lin-Van closed this as completed May 23, 2023
@Lin-Van
Copy link
Author

Lin-Van commented May 23, 2023

@JOU-amjs 好滴,按照您说的改,最后想问下,是不是get请求默认才会缓存,post请求默认不会缓存?

@JOU-amjs
Copy link
Contributor

@Lin-Van 是的,get请求默认有5分钟内存缓存,刷新即清除的,你也可以自定义更改缓存方式,也可以关闭缓存,详见这边

@Lin-Van
Copy link
Author

Lin-Van commented May 23, 2023

@JOU-amjs 如果我要全局关闭缓存是要怎么设置好像没看到说明

@JOU-amjs
Copy link
Contributor

好像是没写到,稍后补充下文档,可以这样全局关闭

createAlova({
  // ...
  localCache: null
})

@Lin-Van
Copy link
Author

Lin-Van commented May 23, 2023

好像是没写到,稍后补充下文档,可以这样全局关闭

createAlova({
  // ...
  localCache: null
})

好滴 收到

@Lin-Van
Copy link
Author

Lin-Van commented May 24, 2023

@JOU-amjs 最后是改成了这样 太南了...

beforeRequest: async (method) => {
    if (getCache(TOKEN_KEY) == null) { //Token过期
            const token = authStore.getRefreshToken;
            await refreshToken({ refreshToken: token })
                .send()
                .then((res) => {
                    if (res.success) {
                        authStore.setToken(res);
                        method.config.headers = assign(method.config.headers, authStore.getAuthorization);
                        resolve(method);
                    }
                });
    } 
},

@JOU-amjs
Copy link
Contributor

是的,我刚也尝试出来了,问题在于ts发现有循环引用了,alova实例引用了refreshToken,而refreshToken又是通过alova实例创建的。

我也是首次遇到这个问题,很不错的解决方法哦😁

@wintsa123
Copy link

@JOU-amjs
image
如果我想再这做个关于token的setgetseesion,我应该如何在这个位置拿到请求的值,我选择了再发送一次,但是这样就重复引用了。。。。

@JOU-amjs
Copy link
Contributor

@wintsa123 我不是很理解你的意思,可以详细描述一下你的使用场景吗?

@glacierck
Copy link

glacierck commented Jul 25, 2023

@Lin-Van 同时并发几个需要权限的接口时,那不是要发送几次刷新请求...?还要就是resolve没有声明怎么能用?这最终版代码跑不了吧?

@Lin-Van
Copy link
Author

Lin-Van commented Jul 26, 2023

@Lin-Van 同时并发几个需要权限的接口时,那不是要发送几次刷新请求...?还要就是resolve没有声明怎么能用?这最终版代码跑不了吧?
这个又不是获取权限,只是获取token,久久一次,又是异步的。resolve就是Promise的resolve。

@glacierck
Copy link

@Lin-Van 同时并发几个需要权限的接口时,那不是要发送几次刷新请求...?还要就是resolve没有声明怎么能用?这最终版代码跑不了吧?
这个又不是获取权限,只是获取token,久久一次,又是异步的。resolve就是Promise的resolve。

更新了新版本后问题消失了,我结合if (!method.meta?.ignoreToken) 进行判断让请求refresh_token的请求不做token检查才能避免beforeRequest死循环,而且还是得保证getCache(TOKEN_KEY)能自检TOKEN_KEY的有效性,其实就是在set的时候加上个有效时间,自动清空缓存。还有就是并发问题,同一个页面多个实例请求要求提供有效Token时,如果Token到期是会出现大家一起触发refresh_token的,可能得考虑做个重发队列,和等待refresh的全局flag,刷完token后重跑阻塞的重发队列。

@Lin-Van
Copy link
Author

Lin-Van commented Jul 26, 2023

@Lin-Van 同时并发几个需要权限的接口时,那不是要发送几次刷新请求...?还要就是resolve没有声明怎么能用?这最终版代码跑不了吧?
这个又不是获取权限,只是获取token,久久一次,又是异步的。resolve就是Promise的resolve。

更新了新版本后问题消失了,我结合if (!method.meta?.ignoreToken) 进行判断让请求refresh_token的请求不做token检查才能避免beforeRequest死循环,而且还是得保证getCache(TOKEN_KEY)能自检TOKEN_KEY的有效性,其实就是在set的时候加上个有效时间,自动清空缓存。还有就是并发问题,同一个页面多个实例请求要求提供有效Token时,如果Token到期是会出现大家一起触发refresh_token的,可能得考虑做个重发队列,和等待refresh的全局flag,刷完token后重跑阻塞的重发队列。

你的思路基本上是对的了,实际上就是一些请求不需要验证token,我是设置了忽略token的请求路径白名单,然后token肯定得有效期自动清空,最后你说的重发队列,也可以有,我这边一个页面是就一个请求就没考虑了。

@JOU-amjs
Copy link
Contributor

JOU-amjs commented Dec 8, 2023

最近实现了个以接口响应为token过期判断依据的功能,发到这边分享一下。

let currentToken = 'expired';
let tokenRefreshing = false; // 正在刷新token,让其他请求先等着
let waitingList = [];
const alovaInst = createAlova({
  baseURL: 'http://example.com',
  statesHook: VueHook,
  requestAdapter: GlobalFetch(),
  beforeRequest: async ({ url, config, meta }) => {
    console.log(url, '正在请求');
    // 如果正在刷新token,则等待刷新完成后再发请求
    if (tokenRefreshing && meta?.authType !== 'refreshToken') {
      console.log('正在刷新token,等待请求');
      await new Promise(resolve => {
        waitingList.push(resolve);
      });
    }
    config.headers.Authorization = currentToken;
  },
  responsed: async (response, method) => {
    if (response.status === 401) {
      // 这边防止请求多次刷新token,把在token刷新完成前发送的拦截并等待
      if (tokenRefreshing) {
        console.log('正在刷新token,等待请求');
        await new Promise(resolve => {
          waitingList.push(resolve);
        });
      })
      console.log(method.url, 'token失效,重新获取token');
      tokenRefreshing = true;
      currentToken = (await refreshToken()).token;
      tokenRefreshing = false;
      waitingList.forEach(resolve => resolve());
      waitingList = [];
      console.log('已获得新token并保存', currentToken, `再重新访问${method.url}`);
      
      // 这里因为是重新请求原接口,与上一次请求叠加会导致重复调用transformData,因此需要将transformData置空去除一次调用
      const methodTransformData = method.config.transformData;
      method.config.transformData = undefined;
      const dataResent = await method;
      method.config.transformData = methodTransformData;
      return dataResent;
    }
    return response.json();
  },
});

然后refreshToken请求method添加了个meta数据才能通过请求

export const refreshToken = () => {
  const method = alovaInst.Get('/refresh-token', {
    localCache: null
  });
  method.meta = {
    authType: 'refreshToken'
  };
  return method;
}

@Lin-Van
Copy link
Author

Lin-Van commented Dec 9, 2023

我的理解是,你希望在beforeRequest中判断token是否过期,过期的话就通过refreshToken刷新token,并继续发送请求。 如果是这样的话,你只需要在beforeRequest中返回这个promise对象就好了,它会等待这个promise resolve后再继续发送请求。

beforeRequest: (method) => {
        if (getCache(TOKEN_KEY) == null) { //Token过期
            return new Promise(resolve => {
                const token = authStore.getRefreshToken;
                const { send: sendRefreshToken } = useRequest(refreshToken, { immediate: false });
                sendRefreshToken({ refreshToken: token })
                    .then((res) => {
                        if (res.success) {
                            authStore.setToken(res);
                            method.config.headers = assign(method.config.headers, authStore.getAuthorization);
                            resolve(method);
                        }
                    });
            });
        } 
    },

是滴 我的需求是正如你所说的那样 谢谢你给的灵感 我稍微修改了下 最后改成这样

beforeRequest: (method) => {
        if (getCache(TOKEN_KEY) == null) { //Token过期
                const token = authStore.getRefreshToken;
                const { send: sendRefreshToken } = useRequest(refreshToken, { immediate: false });
                return sendRefreshToken({ refreshToken: token })
                    .then((res) => {
                        if (res.success) {
                            authStore.setToken(res);
                            method.config.headers = assign(method.config.headers, authStore.getAuthorization);
                            resolve(method);
                        }
                    });
        } 
    },

好的 谢谢分享 这样就可以满足一个页面多次请求不会重复刷新token了

@JOU-amjs
Copy link
Contributor

JOU-amjs commented Dec 15, 2023

最近挺多人在做无感刷新token的,特供一个宠粉福利,@alova/scene-*中新增Token身份认证模块,简单声明配置一下就可以获得完整的Token认证功能了。

https://alova.js.org/tutorial/strategy/tokenAuthentication

mmexport1702646735483.png

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants