You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FastAPI中如何避免并发请求重复刷新Bearer Token?

FastAPI并发场景下的Token刷新优化问题

场景与示例代码

我的FastAPI接口需要调用受保护的外部API,请求必须携带Bearer Token。应用逻辑会先检查当前Token是否过期,若过期则获取新Token。示例代码如下:

from fastapi import FastAPI

app = FastAPI()

class Token:
    def expired(self) -> bool:
        ...

    def __str__(self) -> str:
        ...

def get_token() -> Token:
    ...

async def issue_new_token() -> Token:
    ...

async def touch_external_api(headers: dict):
    ...

@app.post('/process/{task_id}')
async def handler(task_id):
    token = get_token()
    if token.expired():
        token = await issue_new_token()
    await touch_external_api(headers={'Authorization': f'Bearer {token}'})

核心问题

当多个并发请求触发该接口,且token.expired()返回True时,如何确保只有第一个请求调用issue_new_token(),其余请求等待新Token生成完成后直接复用?或者说怎么保证新Token只被获取一次?另外,用后台任务预先更新Token的方案是否可行?

我的尝试(未测试)

我考虑用asyncio的锁原语实现,但感觉不够优雅,代码如下:

import asyncio

token_lock = asyncio.Lock()

async def issue_new_token() -> Token:
    ...

async def issue_token() -> Token:
    if not token_lock.locked():
        await token_lock.acquire()
        token = await issue_new_token()
        token_lock.release()
    else:
        await token_lock.acquire()  # wait until coroutine which issues new token releases
        token_lock.release()
        token = get_token()
    return token

@app.post('/process/{task_id}')
async def handler(task_id):
    token = get_token()
    if token.expired():
        token = await issue_token()
    await touch_external_api(headers={'Authorization': f'Bearer {token}'})

可行方案一:优化asyncio锁实现(解决并发刷新问题)

你的思路方向正确,但原代码存在两处关键问题:一是token_lock.locked需改为方法调用token_lock.locked();二是缺少二次检查——在获取锁后必须再次判断Token是否过期,避免等待锁的过程中已有其他请求完成刷新。

优化后的代码更简洁且健壮:

import asyncio

token_lock = asyncio.Lock()

async def issue_new_token() -> Token:
    # 实现调用外部接口获取新Token的逻辑,并更新全局Token存储
    new_token = ...
    save_token(new_token)  # 假设此函数将新Token存入全局存储,供get_token()读取
    return new_token

async def get_or_refresh_token() -> Token:
    current_token = get_token()
    if not current_token.expired():
        return current_token
    
    async with token_lock:
        # 二次检查:获取锁后再次确认Token状态
        current_token = get_token()
        if current_token.expired():
            current_token = await issue_new_token()
        return current_token

@app.post('/process/{task_id}')
async def handler(task_id):
    token = await get_or_refresh_token()
    await touch_external_api(headers={'Authorization': f'Bearer {token}'})

这里用async with替代手动管理锁的生命周期,能确保锁在异常场景下也会被释放。二次检查是核心逻辑:当多个请求同时检测到Token过期并等待锁时,第一个拿到锁的请求会执行刷新,后续请求获取锁后会发现Token已有效,直接返回即可,彻底避免重复调用issue_new_token()。

可行方案二:后台任务预更新Token(从根源避免并发场景)

后台预更新方案完全可行,适合Token有效期较长的场景。思路是启动一个定时后台任务,定期检查Token剩余有效期,当Token即将过期(比如剩余10%有效期)时提前刷新,确保用户请求到来时Token始终有效。

实现示例:

from fastapi import FastAPI
import asyncio
from contextlib import asynccontextmanager

app = FastAPI()
token_refresh_task: asyncio.Task | None = None

async def token_refresh_background_task():
    while True:
        current_token = get_token()
        # 假设Token总有效期3600秒,剩余不足360秒时触发刷新
        if current_token.expires_in() < 360:
            await issue_new_token()
        # 每隔60秒检查一次
        await asyncio.sleep(60)

@asynccontextmanager
async def lifespan(app: FastAPI):
    global token_refresh_task
    # 启动后台刷新任务
    token_refresh_task = asyncio.create_task(token_refresh_background_task())
    yield
    # 应用关闭时取消任务
    token_refresh_task.cancel()
    try:
        await token_refresh_task
    except asyncio.CancelledError:
        pass

app.router.lifespan_context = lifespan

# Token类、get_token、issue_new_token等其余代码保持不变

@app.post('/process/{task_id}')
async def handler(task_id):
    token = get_token()
    # 保留兜底检查,防止后台任务异常导致Token过期
    if token.expired():
        token = await issue_new_token()
    await touch_external_api(headers={'Authorization': f'Bearer {token}'})

该方案的优势是从根源上消除并发刷新的可能,但需注意:

  • 检查间隔和提前刷新阈值需根据Token实际有效期调整
  • 若为分布式部署(多实例),每个实例会独立运行后台任务,可能导致重复刷新。这种情况需结合分布式锁(如Redis锁)保证只有一个实例执行刷新操作。

内容的提问来源于stack exchange,提问作者hardhypochondria

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 11:38:31