Web API中ConcurrentDictionary跨请求JWT刷新令牌丢失问题排查
问题核心原因
出现“登录时能写入刷新令牌集合、刷新请求时集合为空”的现象,完全符合ASP.NET Core 控制器的默认生命周期规则:
- 框架默认会为每一个传入的HTTP请求,全新创建一个对应的
AuthenticateController类实例,请求处理完成、响应返回后,这个控制器实例就会被销毁回收 - 你代码里的
refreshTokens是直接定义在控制器类里的实例字段,这个字段属于每一个单独的控制器实例,不会在不同请求、不同控制器实例之间共享 - 登录请求时你写入的令牌,存在上一次请求临时生成的控制器实例的字典里,这个实例在登录响应结束后就被回收了;后续的刷新请求拿到的是全新创建的控制器实例,自带的字典本身就是空的,自然读不到之前存的内容
你之前调试时能看到字典写入成功的现象,是因为断点停在同一个请求的同一个控制器实例上下文里,自然能看到刚写入的值,和跨请求读不到的现象完全不冲突。你推测的“Web API隔离每个新请求”的方向是对的,本质就是这个实例化机制导致的状态不共享。
可行的解决方向
按照你的部署和使用场景,可选的实现方案从测试到生产逐级适配:
- 本地快速调试临时方案
直接把refreshTokens字典从实例字段改成静态字段,或者通过依赖注入将其注册为全局单例,让所有控制器实例共享同一个字典对象,测试阶段就能跑通基本的刷新逻辑注意:这个方案仅适合本地单实例调试使用,正式生产环境存在明显缺陷:应用重启、服务部署多副本、扩容多个进程时,存储在进程内存里的字典依然会出现跨实例读不到的问题
- 生产环境标准实现
把刷新令牌的存储逻辑从控制器代码中剥离,抽成独立的令牌仓储层:- 单实例部署的场景,可以直接用框架内置的
IMemoryCache(本身是单例服务,跨请求共享内存存储),设置合理的令牌过期时间自动清理 - 分布式多实例部署的生产场景,建议将刷新令牌存储到Redis、关系型数据库这类可跨服务共享的持久化存储中,同时补充令牌的客户端绑定信息、过期时间、主动作废标记,支持多端登录并行校验、单设备令牌注销的能力
- 单实例部署的场景,可以直接用框架内置的
现有代码的额外提醒
你当前的AddOrUpdate逻辑是每个用户ID只保留最新的一条刷新令牌,一旦用户重新登录,之前发出去的所有旧令牌都会被覆盖失效,如果存在多端同时登录的场景,会出现互相挤掉登录状态的问题,可以在后续迭代中补充每个用户支持多个有效刷新令牌的存储结构。
内容的提问来源于stack exchange,提问作者ArtemiZ Studio
相关产品推荐
相关产品推荐

