ASP.NET Core 8 MVC调用外部API:如何在请求间维护访问令牌?
问题解答
一、请求间维护OAuth访问令牌的正确姿势
1. 用户会话存储的利弊
用会话存令牌实现起来简单,且天然和用户上下文绑定,适合每个用户有独立令牌的场景,但要注意几个问题:
- 默认会话存在服务器内存,多服务器部署时会出现令牌不共享的问题,得改成分布式会话(比如对接Redis)
- 令牌是敏感数据,会话Cookie必须配置
HttpOnly、Secure、SameSite属性,否则容易遭遇XSS或CSRF攻击 - 如果会话过期时间比令牌有效期短,会提前丢失令牌,导致不必要的令牌请求
2. 更推荐的令牌缓存方案
(1)分布式缓存(多服务器必选)
如果你的应用是多实例部署,用分布式缓存按用户标识(比如用户ID)作为键存储令牌和过期时间是最优解:
// 注入IDistributedCache private readonly IDistributedCache _cache; public async Task<string> GetAccessTokenAsync(string userId) { var cacheKey = $"OAuthToken:{userId}"; var tokenData = await _cache.GetStringAsync(cacheKey); if (tokenData == null) { // 调用授权服务器获取新令牌 var newToken = await RequestNewTokenAsync(); // 缓存时预留5分钟缓冲,避免令牌刚过期就请求失败 await _cache.SetStringAsync(cacheKey, newToken, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(55) }); return newToken; } return tokenData; }
(2)框架内置的令牌缓存
如果你的应用是用AddOAuth或AddOpenIdConnect配置的外部认证,框架会自动把令牌存在AuthenticationProperties中,直接通过HttpContext.GetTokenAsync就能获取:
// 控制器内获取令牌 var accessToken = await HttpContext.GetTokenAsync("你的OAuth方案名", "access_token"); if (string.IsNullOrEmpty(accessToken)) { // 令牌失效,触发重新认证流程 return Challenge("你的OAuth方案名"); }
这种方式的令牌会加密存储在Cookie中,也可配置为持久化到分布式缓存,无需自行编写缓存逻辑。
3. 令牌刷新要提前处理
令牌有效期1小时,建议在缓存中存储令牌的过期时间,每次获取令牌时检查如果剩余时间不足10分钟,就自动触发刷新,避免请求被拒绝后再处理。
二、IHttpClientFactory + 类型化客户端对令牌维护的影响
改用IHttpClientFactory绝对是正确的选择,不仅能解决手动创建HttpClient导致的socket耗尽问题,还能让令牌维护更省心:
1. 用DelegatingHandler自动附加令牌
编写自定义的DelegatingHandler,在发送请求前自动获取令牌并添加到请求头中,控制器无需再手动处理令牌逻辑:
public class OAuthTokenHandler : DelegatingHandler { private readonly ITokenService _tokenService; private readonly IHttpContextAccessor _httpContextAccessor; public OAuthTokenHandler(ITokenService tokenService, IHttpContextAccessor httpContextAccessor) { _tokenService = tokenService; _httpContextAccessor = httpContextAccessor; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; var accessToken = await _tokenService.GetAccessTokenAsync(userId); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); return await base.SendAsync(request, cancellationToken); } } // 注册类型化客户端时添加该Handler services.AddHttpClient<IMyExternalApiClient, MyExternalApiClient>() .AddHttpMessageHandler<OAuthTokenHandler>();
2. 和令牌缓存逻辑完全解耦
类型化客户端只是封装了HttpClient的创建和配置,你之前考虑的令牌缓存方案(会话、分布式缓存)都能直接使用,完全不冲突,只需将缓存逻辑封装在ITokenService中即可。
3. 统一处理令牌过期重试
在DelegatingHandler中还能捕获401未授权响应,自动刷新令牌并重试请求,无需在每个控制器动作中编写重复的重试逻辑:
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; var accessToken = await _tokenService.GetAccessTokenAsync(userId); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var response = await base.SendAsync(request, cancellationToken); if (response.StatusCode == HttpStatusCode.Unauthorized) { // 刷新令牌 var newToken = await _tokenService.RefreshTokenAsync(userId); // 更新请求头 request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken); // 重试一次请求 response = await base.SendAsync(request, cancellationToken); } return response; }
总结
- 令牌维护优先选择分布式缓存(多服务器场景)或框架内置的AuthenticationProperties缓存(单服务器场景),使用会话存储时一定要做好安全配置和分布式适配。
- 改用IHttpClientFactory + 类型化客户端是最佳实践,既能解决HttpClient的资源占用问题,还能通过DelegatingHandler统一处理令牌相关逻辑,让控制器代码更简洁。
内容的提问来源于stack exchange,提问作者patsy2k
相关产品推荐
相关产品推荐

