.Net Core 3.1用RestSharp调用API的Token过期复用异常排查
现有代码问题分析与适配方案
现有代码的核心逻辑缺陷
- 缓存写入未绑定Token实际过期时间:调用
_memoryCache.Set()时没有配置和第三方Token有效期匹配的过期规则,既可能因内存缓存压力被系统自动清理,也可能出现第三方Token早已失效、本地仍反复使用旧值的情况 - 存在缓存击穿并发风险:当缓存中Token失效/被清理的瞬间,多个并发请求会同时触发Token重获取逻辑,短时间内生成多个全新Token。如果第三方API对同一账号有“仅最近一次生成的Token生效、旧Token立即作废”的策略,就会导致已生成的旧Token全部失效,所有依赖旧Token的请求全部失败,这正是“接口有时完全正常、有时全部失败”的核心诱因
- 状态判断逻辑错误:
CancelCertificateController中判断第三方接口响应状态时,错把控制器自身的Response对象(当前接口返回给前端的响应,默认状态码200)当成了第三方返回的IRestResponse状态码,完全丧失了异常识别能力 - 结构耦合度高:所有接口控制器通过继承
HomeController复用Token获取逻辑,代码重复、维护成本高,无法全局统一管控Token的刷新、异常处理流程
可满足「Token仅过期后重生成、其余时间全局复用」要求的实现方案
核心思路是将Token获取逻辑抽离为全局单例服务,通过信号量锁解决并发击穿问题,将缓存过期时间与第三方Token的实际有效期严格绑定。
1. 定义独立认证服务
首先将Token管理逻辑从控制器中完全剥离,避免控制器继承带来的耦合:
// 认证服务接口 public interface IThirdPartyAuthService { Task<string> GetValidTokenAsync(); } // 认证服务实现,注册为单例 public class ThirdPartyAuthService : IThirdPartyAuthService { private readonly IMemoryCache _memoryCache; private readonly ILogger<ThirdPartyAuthService> _logger; // 全局信号量锁,确保同一时间只有一个线程能执行Token刷新逻辑 private readonly SemaphoreSlim _tokenRefreshLock = new SemaphoreSlim(1, 1); private const string TokenCacheKey = "ThirdPartyApi:AuthToken"; // 第三方Token接口配置 private const string TokenRequestUrl = "xx"; private const string Username = "xx"; private const string Password = "xx"; public ThirdPartyAuthService(IMemoryCache memoryCache, ILogger<ThirdPartyAuthService> logger) { _memoryCache = memoryCache; _logger = logger; } public async Task<string> GetValidTokenAsync() { // 优先从缓存读取有效Token if (_memoryCache.TryGetValue(TokenCacheKey, out string cachedToken) && !string.IsNullOrEmpty(cachedToken)) { return cachedToken; } // 加锁,阻塞其他等待刷新的请求 await _tokenRefreshLock.WaitAsync(); try { // 双重检查:拿到锁后再次确认缓存是否已有新Token,避免重复刷新 if (_memoryCache.TryGetValue(TokenCacheKey, out cachedToken) && !string.IsNullOrEmpty(cachedToken)) { return cachedToken; } // 调用第三方接口获取新Token var client = new RestClient(TokenRequestUrl); var request = new RestRequest(Method.POST) { RequestFormat = DataFormat.Json }; request.AddParameter("grant_type", "password"); request.AddParameter("username", Username); request.AddParameter("password", Password); var response = await client.ExecuteAsync<MyToken>(request); if (response.StatusCode != System.Net.HttpStatusCode.OK || response.Data == null || string.IsNullOrEmpty(response.Data.token)) { _logger.LogError("获取第三方Token失败,状态码:{StatusCode}, 错误:{Error}", response.StatusCode, response.ErrorMessage); throw new InvalidOperationException("获取第三方认证Token失败"); } // 计算缓存过期时间:第三方返回的expires_in为有效期秒数,预留30秒提前量,避免网络延迟导致请求时刚好过期 var cacheExpireTime = TimeSpan.FromSeconds(response.Data.expires_in - 30); if (cacheExpireTime <= TimeSpan.Zero) { cacheExpireTime = TimeSpan.FromSeconds(response.Data.expires_in); } // 写入缓存,绑定绝对过期规则 _memoryCache.Set(TokenCacheKey, response.Data.token, new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheExpireTime, Priority = CacheItemPriority.Normal }); return response.Data.token; } finally { // 释放锁资源 _tokenRefreshLock.Release(); } } }
2. 服务注册
在Startup.cs的服务配置方法中,将认证服务注册为单例,确保全局所有请求拿到的是同一个服务实例、同一份Token缓存:
public void ConfigureServices(IServiceCollection services) { services.AddMemoryCache(); // 注册认证服务为单例 services.AddSingleton<IThirdPartyAuthService, ThirdPartyAuthService>(); services.AddControllers(); }
3. 修正业务接口逻辑
去掉控制器对HomeController的继承,直接注入认证服务获取有效Token,同时修正之前的状态码判断错误:
[Route("api/Certificate/Cancel")] [Produces("application/json")] [ApiController] public class CancelCertificateController : ControllerBase { private readonly IThirdPartyAuthService _authService; private readonly ILogger<CancelCertificateController> _logger; public CancelCertificateController(IThirdPartyAuthService authService, ILogger<CancelCertificateController> logger) { _authService = authService; _logger = logger; } [HttpPost] [ProducesResponseType(200)] [ProducesResponseType(400)] public async Task<IActionResult> Post([FromBody] CancelCert cancelCert) { try { var validToken = await _authService.GetValidTokenAsync(); var client = new RestClient("xx"); var request = new RestRequest(Method.POST); request.AddHeader("Accept", "application/json"); request.AddHeader("Content-Type", "application/json; charset=utf8"); request.AddHeader("Authorization", $"Bearer {validToken}"); request.AddHeader("ClientID", "xxx"); request.AddJsonBody(cancelCert); IRestResponse response = await client.ExecuteAsync(request); // 正确判断第三方接口的返回状态 if (response.StatusCode == System.Net.HttpStatusCode.OK) { var jObject = JObject.Parse(response.Content); return new JsonResult(jObject); } _logger.LogWarning("调用第三方作废证书接口失败,状态码:{StatusCode}, 返回内容:{Content}", response.StatusCode, response.Content); return BadRequest($"第三方接口调用失败,状态码:{(int)response.StatusCode}"); } catch (Exception ex) { _logger.LogError(ex, "作废证书接口处理异常"); return StatusCode(500, "服务器内部错误"); } } }
方案匹配性说明
- 全局单例的缓存机制保证所有接口从同一份缓存中读取Token,只要缓存未过期就完全复用,不会出现Token提前重生成的问题
- 双重检查的信号量锁从根本上解决了缓存击穿问题,缓存失效的瞬间仅单个请求会触发Token刷新,其余请求等待锁释放后直接读取已写入的新Token,不会触发第三方的多Token作废机制
- 缓存过期时间与第三方Token实际有效期严格绑定,预留少量提前量避免边界场景,既不会提前替换未过期的Token,也不会使用已失效的旧Token
- 逻辑与控制器完全解耦,所有需要调用第三方API的接口仅需注入
IThirdPartyAuthService即可获取有效Token,无需重复写Token获取逻辑
内容的提问来源于stack exchange,提问作者Nickson
相关产品推荐
相关产品推荐

