惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
S
Security @ Cisco Blogs
S
Secure Thoughts
Attack and Defense Labs
Attack and Defense Labs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Hacker News - Newest:
Hacker News - Newest: "LLM"
Recent Commits to openclaw:main
Recent Commits to openclaw:main
H
Hacker News: Front Page
博客园 - 叶小钗
H
Heimdal Security Blog
Microsoft Security Blog
Microsoft Security Blog
Forbes - Security
Forbes - Security
AI
AI
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Troy Hunt's Blog
罗磊的独立博客
Application and Cybersecurity Blog
Application and Cybersecurity Blog
爱范儿
爱范儿
GbyAI
GbyAI
The Last Watchdog
The Last Watchdog
TaoSecurity Blog
TaoSecurity Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
D
DataBreaches.Net
Recent Announcements
Recent Announcements
Schneier on Security
Schneier on Security
C
Cisco Blogs
美团技术团队
D
Docker
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
月光博客
月光博客
雷峰网
雷峰网
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
H
Hackread – Cybersecurity News, Data Breaches, AI and More
A
Arctic Wolf
B
Blog RSS Feed
Cisco Talos Blog
Cisco Talos Blog
C
Cybersecurity and Infrastructure Security Agency CISA
V
Vulnerabilities – Threatpost
V2EX - 技术
V2EX - 技术
Y
Y Combinator Blog
N
News and Events Feed by Topic
www.infosecurity-magazine.com
www.infosecurity-magazine.com
W
WeLiveSecurity
Security Archives - TechRepublic
Security Archives - TechRepublic
G
GRAHAM CLULEY
Jina AI
Jina AI
Hugging Face - Blog
Hugging Face - Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
The Hacker News
The Hacker News

JavaScript

发现国内很多文章对宏任务和微任务的解释都是有问题的 - V2EX 智能香氛机小程序外包合作招募 - V2EX 分享一道 Node.js 的面试题,考察 JS 相关的最基本的掌握程度,代码完全来自官方文档 你们 js 用过双等号吗 - V2EX 有人大量建立技术文档的.cn 中文站点,并将站内 js 链接指向 cdn.jsdelivr.net.cn - V2EX 发现了两个关于 JavaScript 语言好玩的网站,让人血压升高 js 判空值 最佳实践 - V2EX 简单的图像算法为什么 AI 搞不定呢 Capacitor.JS 如何自动化最后一步流程 JSON.parse 转数字精度丢失问题 有没有前端大佬,帮我看一下这个网站底部的粒子动画怎么做的。 - V2EX nstr - number → string, but looks good 前端 js 播放 base64 编码的 mp3,放出来杂音很大,请教是哪里出错了? 熟悉工业计算的来,是阀门相关的企业 js 不使用 promise 完成一个 sleep 函数,必须支持 await 调用 - V2EX 小米手机澎湃系统 AutoJS 的悬浮窗在证券软件打开时就消失 - V2EX 关于 js 复制粘贴问题 求助,无法在 wixsite.com 上使用 document.write threejs 官方文档看着好难受。 - V2EX 有没有 AI 能一步一步教会伸手党如何使用业务系统? 也聊下自己最近用 copilot 写程序的心得,还要讨教下方案 javascript 继续 套壳 AI 应用类 manus 如何在移动端 h5 页面实现<Select/>搜索下拉选择框! - V2EX 求前端高频数据绘图库 - V2EX 吐槽一下 Trae 记忆中好像在论坛看到有款专门在 iframe 中做前端的 js 库/框架? - V2EX JavaScript 中的 Promise 跟异步有关系吗?还是我的理解有问题?谁能把 Promise 解释清楚? Promise 的正确用法应该是什么样的? - V2EX 求问,小程序的菜单栏怎么和右边的功能键垂直居中? - V2EX 自习室小程序的座位图是如何实现的(前端) - V2EX 谷歌浏览器的 bug - V2EX 为什么中小公司的前端面试官爱问 array 和 object 有多少种方法 - V2EX 有什么办法能实现 uniapp 上的离线人脸识别 本人遇到了非常奇怪的关于“注释”ctrl+/ 快捷键问题,需要大佬们指点一二。 ChatGPT 语音对话技术 求前端大佬解惑, HTML 里的文本怎么做逐行滚动? 快应用问题求助 2024 JS 生态调查结果出炉: React 遥遥领先, Vue 程序员最穷 不顾“万人联名请愿”,甲骨文拒绝放弃 JavaScript 商标所有权 2025 年,我又把前端"老古董"--AMD 给翻了出来 有使用 javascript 编写 windows cmd 批处理程序的方法吗,类似 coffescript 这样转译的东西 js 小白,请教一下为什么网页上的播放器, Chrome 控制台不能用 querySelector 直接获取到呢 自荐一个 js 通信的包 Python 代码转换为 Javascript 运算结果不一致 css 的 justify-content 等效语法? 请教一下使用油猴脚本修改 html 内容后, 样式不生效的问题 async 函数中止/task local 方案后记,V8 上的另一个可行思路 如何在前端统计用户观看视频所消耗的流量?求教啊,大神们! pdfjs 中,通过 findController 可以搜索到对应的文本信息,并且高亮出来。那么, pdfjs 有没有什么 api 可以获取搜索到的信息的位置节点 ?请教 有没有独立的同时可以 pc 移动端用的三级联动城市选择组件啊 - V2EX 字幕滚动有没有更好的方式实现
这段前端代码存在并发读取竞态的问题吗?
supuwoerc · 2025-09-04 · via JavaScript

这是一个创建于 282 天前的主题,其中的信息可能已经有所发展或是发生改变。

这两天调试别人项目中的一段 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)
            }
        })

第 1 条附言  ·  2025 年 9 月 4 日

楼下已经有几位大佬给出了分析,是请求返回时序问题导致的,并非对变量的判断导致,感谢各位大佬的解答!

irisdev

1

irisdev      2025 年 9 月 4 日   ❤️ 1

想想也知道存在并发啊。。一个页面发十个请求,浏览器会等十个请求全部完成再加载页面吗。。

chairuosen

2

chairuosen      2025 年 9 月 4 日

多个 tab ,或者框架网页,就会出现

crazzy

3

crazzy      2025 年 9 月 4 日

这都是异步,肯定会出现一个请求检查完 isRefreshing 准备赋值前,另一个请求也在检查 isRefreshing 了。

一个异步的整个生命周期中,事件循环会处理其他任务

IWSR

4

IWSR      2025 年 9 月 4 日

axios 设置拦截器会在每个 HTTP 请求发送时被调用,如果你前端代码内存在同时发送多个请求的逻辑那拦截器内的回调会多次触发,所以多次刷新 token 的现象出现不奇怪,解决办法就是增加一个全局变量去标记刷新 token 这个操作是否已经被触发,如果被触发就不执行刷新 token 的逻辑

zhhcnn

5

zhhcnn      2025 年 9 月 4 日

用 promise 加锁就行了,读 token 之前 await 一下等 promise 返回

geelaw

6

geelaw      2025 年 9 月 4 日

不存在针对 isRefreshing 的竞态条件,原因很简单:JavaScript 没有多线程,而只有 JavaScript 代码会读写 isRefreshing 。

和 worker 也没有任何关系,因为 worker thread 之间不共享对象,要共享数据必须 postMessage 。

supuwoerc

7

supuwoerc      2025 年 9 月 4 日

@IWSR 代码里面的 isRefreshing 就是全局变量啊🤔️,只是依靠这个变量也卡不住

irisdev

8

irisdev      2025 年 9 月 4 日

@chairuosen #2 老板,多 tab 这种情况把 isrefreshing 放在 localstorage 里面共享一下是不是就行了

JoeJoeJoe

10

JoeJoeJoe      2025 年 9 月 4 日

虽然 js 不开 worker 是单线程的, 但是你的请求是异步的, 也就是说你的刷新 token 也是异步的, 不会阻塞后续的其他请求, 在等待新 token 的时候, 其他的接口如果有返回值回来, 同样, 他们的 token 也是过期的, 也是会触发你的刷新 token 逻辑的.
比如: 现在的 token 是过期的, 同时发送了 A,B,C3 个请求
1. A 请求发送之后, 接口返回 token 过期, 然后刷新 token, 获取到新的 token, 如果这期间 B,C 还没有发出去, 那么是没有问题的.
2. A 请求发送之后, B,C 请求同时发出去了, 那么那么 BC 请求带的 token 也是过期 Token, 也会触发你说的现象.
3. A 请求发送之后, BC 请求还没有发送, A 请求收到了 token 过期的响应, 开始请求刷新 token, 这期间 BC 请求发出去了, 这种场景也会触发你说的现象.

ps: 我大概猜的, 佬们轻喷

ccccccc

11

ccccccc      2025 年 9 月 4 日

我建议不要搞得那么复杂,过期该重新登录就重新登录。用户根本不 care 重新登录带来的不方便,除非你几分钟就要用户登录一次

supuwoerc

12

supuwoerc      2025 年 9 月 4 日

@ccccccc 这个是别人的项目我帮着修 bug 的,物流程序,token 有效期只有 5 分钟,用的长短 token 方案,不断重新登录应该是要爆炸的😂

supuwoerc

13

supuwoerc      2025 年 9 月 4 日

@JoeJoeJoe 应该不是同时发出去,而是同时返回,同时执行到 if (!isRefreshing)的判断才会引发这个问题,我理解是这样的。

chairuosen

14

chairuosen      2025 年 9 月 4 日

@irisdev 不是,他这个逻辑是在加锁的刷新线程里最后处理其他等待线程的后续,多 tab 是处理不了的,得改造一下等待的线程轮询锁自己处理后续

geelaw

15

geelaw      2025 年 9 月 4 日

@chairuosen #2 每个 tab 里都有自己的 isRefreshing 。

@crazzy #3 在 if (!isRefreshing) 和 isRefreshing = true 之间不可能有任何 JavaScript 代码执行。

@IWSR #4 这个好像是楼主代码的意思。

@supuwoerc #9

楼主可以多考虑一下读者,尝试简化代码,使之不需要借助外部语境就很容易理解,比如我们不知道 user.useLoginStore.getState() 的状态是否是 session/local 级别的,还是 tab 级别的。如果 user.useLoginStore.getState() 是 session/local 的状态,那么不同的 tab 有自己的 isRefreshing ,当然两个 tab 可以同时看到过期的 token 并分别决定刷新。

@irisdev #8 放在 local/session 都不好,因为用户可以在 tab 1 刷新的时候关闭之,但 tab 1 通过共享的 isRefreshing 锁定了刷新的权力,这会导致 local/session 清空之前不再有任何 tab 会进入刷新的逻辑。

kamilic

17

kamilic      2025 年 9 月 4 日

isRefreshing 肯定是能锁着的,都是单个线程执行的,cursor 和 copilot 说的没错。
我推测这种情况是不是并发发出的时候,出现这种情况:
| ----- req1 / res1 / refresh ----- | ------------------------ isRefreshing = false ------------------------------------ |
| ----- req2 ------------------------------------------------------------- | --------- res2 / isRefreshing = false ------ |

有可能你并发 req1 / req2 都是返回 10003 的,恰好 req1 刷新 token 后状态还原 isRefreshing ,req2 才返回 10003 ,那不就再刷一次喽。
是不是你的 requests 变量要再记录下在 isRefreshing = true 过程中的所有待返回请求?

supuwoerc

18

supuwoerc      2025 年 9 月 4 日

@geelaw 感谢大佬,user.useLoginStore.getState()是从 zustand 中读存储的 token/refresh_token 的,我复现的情况没有开多个 tab ,单独一个 tab 就偶发的出现多次刷新 token 的行为。

Niphor

19

Niphor      2025 年 9 月 4 日

F12 network 里多看看就知道拉,并发的多个请求+长短不一的响应时间 就会触发这个

LuckyRock

20

LuckyRock      2025 年 9 月 4 日

假设 token 过期,发了 A 和 B 两个请求,A 请求收到响应之后过期,刷新 token ,刷新 token 的请求先于 B 请求返回,那么此时 isRefreshing 被重置为 false ,然后 B 请求(携带过期 token )返回了,自然就重新走了刷新 token 的逻辑。

Niphor

21

Niphor      2025 年 9 月 4 日

根源是 有的请求已经进 finally 了,但是有的请求 response 刚进 interceptor

InDom

22

InDom      2025 年 9 月 4 日

isRefreshing 这个锁不应该是 bool 型 ,而应该是最少三个状态

false, refreshing, true

更好的方案是记录 refreshing 的时间, 再加一个超时的机制.

如果发现 true 就直接使用, 如果是 false 就设置为当前时间并开始更新 token,

如果是 refreshing 的时间, 就检查是否超时(指上一个任务太久未完成)

如果 refreshing 超时, 就自己重新设置 refreshing 时间并开始更新 token

如果 refreshing 未超时, 就设置个随机时间后重新检查 isRefreshing 状态.

supuwoerc

23

supuwoerc      2025 年 9 月 4 日

@kamilic 醍醐灌顶!我觉得这个分析才解释了真正导致问题的原因,我之前已经怀疑人生了,没想到这种时序导致的问题。

duuu

24

duuu      2025 年 9 月 4 日

你的 isRefreshing 不要做成全局的,改成单个接口自己判断使用。

mrzhiin

26

mrzhiin      2025 年 9 月 4 日

当接口的响应为令牌过期错误的时候,在刷新令牌之前,先判断下接口使用的令牌和当前状态里的令牌是否一致,如果不一致,替换为当前状态里令牌重新再发起请求

LOWINC

27

LOWINC      2025 年 9 月 4 日   ❤️ 1

let isRefreshing = false;

function request(success, time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (success) {
resolve({});
} else {
reject();
}
}, time);
});
}

Promise.all([
request(false, 100)
.then(() => {})
.catch(() => {
if (!isRefreshing) {
isRefreshing = true;
console.log('refresh1:', isRefreshing);
setTimeout(() => {
isRefreshing = false;
}, 100);
}
}),
request(false, 3000)
.then(() => {})
.catch(() => {
if (!isRefreshing) {
isRefreshing = true;
console.log('refresh2:', isRefreshing);
setTimeout(() => {
isRefreshing = false;
}, 3000);
}
}),
]);

可能就是两次 10003

canvascat

29

canvascat      2025 年 9 月 4 日

不用加锁,复用一下 `refreshTokenPromise`应该就行了

```
declare const orginalRefreshToken: () => Promise<any>;

const refreshToken = () => {
refreshToken.current ??= orginalRefreshToken().finally(() => {
refreshToken.current = null;
});
return refreshToken.current;
};

refreshToken.current = null as Promise<any> | null;
```

NotLongNil

31

NotLongNil      2025 年 9 月 4 日

这段代码一堆 if ,看得好难受

bli22ard

32

bli22ard      2025 年 9 月 4 日   ❤️ 1

js 单线程没有并发读写问题,js 的 io 都是异步的,只有开了异步后,异步唤醒后的顺序问题。在执行 if (!isRefreshing) {
isRefreshing = true; } 这个过程中,不会有其他线程来竞争, 但是如果你执行了 settimeout setinterval 或者 ajax 等异步,那就会去检查其他事件是否就绪,然后执行其他的地方。

leoskey

33

leoskey      2025 年 9 月 4 日   ❤️ 1

在多个请求响应时间不同的情况下,可能会出现重复刷新 token 。

例如,A 请求响应时间 100ms ,B 请求响应时间 300ms 。
A 请求响应 token 过期,在 100ms 内完成: isRefreshing = true , 执行 refresh 流程,isRefreshing = false 。
此时才过去 200ms 。

再过 100ms 后,B 请求响应 token 过期,发现 isRefreshing = false ,再次执行 refresh 。

试试双重检查:
1. 如果是 JWT ,先检查本地 token 是否过期
2. 如果服务器响应 token 无效
3. 再检查一次本地 token 是否与上次相同,不相同说明刷新过了,相同就执行 refresh

shintendo

34

shintendo      2025 年 9 月 4 日

用一个 outdatedToken 变量代替 isRefreshing

if (!isRefreshing){ isRefreshing = true }
换成
if(outdatedToken !== config.token) { outdatedToken = config.token }

去掉 isRefreshing = false 的操作

其它不变

GGbeng1

35

GGbeng1      2025 年 9 月 4 日

还有一种方法,过期时将所有请求地址记录;同时 cancel 掉所有请求;刷新后从新发送或者直接刷新页面😂

jchencode

36

jchencode      2025 年 9 月 4 日

没用 AI 去理解代码吗

dcatfly

37

dcatfly      2025 年 9 月 4 日

这个问题打断点调试应该很快就能发现是时序的问题。
不过这个问题还挺适合测 AI 的,拿给各家 AI 问了下,只有 gpt5 thinking 能一次给对答案,claude sonnet/opus4 需多轮对话、gemini 、qwen3/glm4.5/k2 都不能给出正确答案。

unclejoker

38

unclejoker      2025 年 9 月 4 日

丢给 gpt 一下不就得出答案了吗...

xibexp

39

xibexp      2025 年 9 月 5 日

有没有返回 token 过期时间,有的话直接在外部定时判断是否快过期,然后续期