YARP认证:如何实现令牌刷新?附场景与代码示例
YARP代理中Service1端令牌刷新的最佳实现方式
你的代码问题分析
你当前的实现会导致502错误,核心原因是:
- 错误使用了未定义的
_next变量,应该用传入的next委托 - 第一次调用
next后,HttpContext的请求/响应流已被消耗、状态已被修改,无法直接重复调用next重发请求,这会导致代理无法正确处理二次请求,返回网关错误
最佳实现方式:自定义代理中间件+令牌缓存服务
推荐通过封装令牌刷新逻辑的独立服务+自定义代理中间件实现,核心思路是:先尝试用有效令牌发送请求,收到401时刷新令牌、重置请求上下文并重发,同时缓存有效令牌避免重复刷新。
1. 封装令牌管理服务
先实现一个负责令牌获取、刷新、缓存的服务,避免并发场景下重复调用刷新接口:
public interface ITokenService { Task<string> GetValidTokenAsync(); Task<string> RefreshTokenAsync(); } public class TokenService : ITokenService { private readonly HttpClient _httpClient; private string _currentToken; private DateTime _tokenExpiry; private readonly SemaphoreSlim _refreshLock = new SemaphoreSlim(1, 1); public TokenService(HttpClient httpClient) { _httpClient = httpClient; } public async Task<string> GetValidTokenAsync() { await _refreshLock.WaitAsync(); try { // 令牌为空或即将过期时刷新 if (string.IsNullOrEmpty(_currentToken) || DateTime.UtcNow >= _tokenExpiry) { _currentToken = await RefreshTokenAsync(); // 解析JWT令牌的过期时间,提前1分钟刷新避免过期 var jwtHandler = new JwtSecurityTokenHandler(); var jwtToken = jwtHandler.ReadJwtToken(_currentToken); _tokenExpiry = jwtToken.ValidTo.AddMinutes(-1); } return _currentToken; } finally { _refreshLock.Release(); } } public async Task<string> RefreshTokenAsync() { // 调用Service1的令牌刷新接口,根据实际业务调整请求参数 var refreshResponse = await _httpClient.PostAsync("/api/auth/refresh", null); refreshResponse.EnsureSuccessStatusCode(); var tokenResult = await refreshResponse.Content.ReadFromJsonAsync<TokenResponse>(); return tokenResult.AccessToken; } } public class TokenResponse { public string AccessToken { get; set; } }
2. 正确实现代理中间件
在YARP代理管道中添加自定义中间件,处理401时的令牌刷新与请求重发:
endpoints.MapReverseProxy(proxyPipeline => { proxyPipeline.Use(async (context, next) => { var tokenService = context.RequestServices.GetRequiredService<ITokenService>(); // 首次请求前设置有效令牌 var initialToken = await tokenService.GetValidTokenAsync(); context.Request.Headers.Authorization = $"Bearer {initialToken}"; // 保存原始请求体(请求流只能读取一次,重发时需要重置) var originalRequestBody = await ReadRequestBody(context.Request); // 第一次发送请求 await next(context); // 收到401时尝试刷新令牌并重发 if (context.Response.StatusCode == StatusCodes.Status401Unauthorized) { // 刷新令牌 var newToken = await tokenService.RefreshTokenAsync(); // 重置响应上下文(清空已写入的响应内容) context.Response.Clear(); // 更新请求头的令牌 context.Request.Headers.Authorization = $"Bearer {newToken}"; // 重置请求体流 if (originalRequestBody != null) { context.Request.Body = new MemoryStream(originalRequestBody); context.Request.ContentLength = originalRequestBody.Length; } // 重新执行代理管道,发送更新令牌后的请求 await next(context); } }); proxyPipeline.UseSessionAffinity(); proxyPipeline.UseLoadBalancing(); proxyPipeline.UsePassiveHealthChecks(); }); // 辅助方法:读取并保存请求体内容 private async Task<byte[]> ReadRequestBody(HttpRequest request) { if (!request.ContentLength.HasValue || request.ContentLength == 0) return null; using var memoryStream = new MemoryStream(); await request.Body.CopyToAsync(memoryStream); memoryStream.Position = 0; request.Body = memoryStream; return memoryStream.ToArray(); }
3. 关键注意事项
- 用
SemaphoreSlim控制令牌刷新的并发,避免多个请求同时触发刷新接口 - 必须保存并重置请求体流,ASP.NET的请求流是单次读取的,重发请求时需要重新提供流内容
- 收到401后要先清空响应上下文,否则会导致响应内容混乱
- 令牌过期时间建议提前1分钟刷新,避免网络延迟导致令牌在请求过程中过期
内容的提问来源于stack exchange,提问作者Andrey
相关产品推荐
相关产品推荐

