这两天调试别人项目中的一段 js 代码,作用是刷新 token ,但是验证下来发现有很小的几率会触发多次刷新 token 的动作(下面代码中的 FIXME 位置),特别是 Promise.all 去发送一批请求的时候,我 google 了一圈,没研究明白,因为复现起来很困难,所以请教大家,代码中读取 isRefreshing 是安全的吗?我让 cursor 和 copilot 解释都是说 js 不启用 worker 是不存在并发问题的,但是从结果来看,确实有不止一个请求进入了刷新 token 的分支,我把这个情况描述完,cursor 让我引入 sync-mutex 加锁,和一开始的解释完全不一样,我在 StackOverflow 和 medium 中也找到几篇类似的文章,都是借助了防抖/记忆函数来解决,实在弄不清楚这块读写 isRefreshing 到底是不是安全的。
还看到了一篇锁的文章,感觉很类似我遇到的这个问题: https://jackpordi.com/posts/locks-in-js-because-why-not
伪代码如下:
let isRefreshing = false // 标记是否正在刷新 token
let requests: Array<(token: string, err?: string) => void> = [] // 需要重试的请求列表
client.interceptors.response.use((response: AxiosResponse) => {
const { config, status } = response
const { code } = response.data
if (status >= 500) {
return Promise.reject("服务器错误")
} else if (code == 10003) {
// access token 过期,尝试刷新 token
const { refreshToken } = user.useLoginStore.getState()
if (refreshToken) {
// FIXME: ?? 存在并发读取 isRefreshing 为 false 导致发出多次刷新 token 的请求
if (!isRefreshing) {
isRefreshing = true
return refreshToken()
.then(({ data }) => {
const { code } = data
if (code === 10000) {
user.useLoginStore.setState((state) => {
state.token = data.data.token
})
config.headers["Authorization"] = data.data.token
const retry = client(config)
requests.forEach((cb) => cb(data.data.token))
requests = []
return retry
} else {
return Promise.reject(data.message)
}
})
.catch((err) => {
const msg = isError(err) ? err.message : err
requests.forEach((cb) => cb("", msg))
requests = []
publishInvalidTokenEvent(msg)
})
.finally(() => {
isRefreshing = false
})
} else {
return new Promise((resolve, reject) => {
requests.push((token: string, err?: string) => {
if (err) {
reject(err)
} else {
config.headers["Authorization"] = token
resolve(client(config))
}
})
})
}
} else {
requests.forEach((cb) => cb("", "登录过期")
requests = []
publishInvalidTokenEvent("登录过期")
}
} else if (code === 10000) {
return response.data.data
} else if (code == 10006) {
// 长 token 失效
requests.forEach((cb) => cb("", "登录过期")
requests = []
publishInvalidTokenEvent("登录过期")
} else {
return Promise.reject(response.data.message || response.data.msg)
}
})
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.