我们在项目中集成了Refit,但是在调用接口时,出现了问题,提示未授权的访问。这个问题是怎么导致的呢?我们该怎么处理呢?在这篇文章中我们一起来解决吧。
一、为什么会出现这个问题
让我们来深入分析一下是哪里返回的未授权的访问的提示。首先,我们采用最直接的方法:在IDE中全局搜索整个项目中包含这个提示的代码。经过搜索,我们发现这个提示信息只存在于通用中间件ApplicationMiddleware
中。
通过进一步分析代码执行流程,我们定位到问题的根源在于 验证网关签名 环节未能通过验证。为了更深入地了解这个问题,我们需要详细检查验证网关签名的方法ValidateGatewaySignature
的执行过程。我们采用断点调试的方式,在该方法的入口处设置断点,然后通过Refit发送测试请求。当请求命中断点时,通过调试器我们清晰地观察到一个关键问题:在请求头中完全不存在X-Gateway-Signature
这个关键参数。这就直接导致了验证失败,系统返回未授权访问的提示。
那么,为什么会出现这个问题呢?这涉及到我们系统的架构设计。在正常的访问流程中,所有的请求都应该经过网关进行转发和处理。但是在使用Refit进行服务间通信时,我们采用了一个不同的路径:Refit直接从Nacos注册中心获取了一个健康的服务实例,然后直接向该实例发起请求。这种直接通信的方式绕过了网关,导致请求头中缺少了网关在转发时应该添加的X-Gateway-Signature
签名信息。
要解决这个问题,我们有一个相对简单的解决方案:在使用Refit发起请求时,手动添加X-Gateway-Signature
到请求头中。这样就能确保请求携带了必要的签名信息,从而通过中间件的验证。这个修改虽然简单,但需要注意保持签名生成的逻辑与网关端保持一致,以确保验证能够顺利通过。
二、解决问题
为了解决这个问题,我们需要对RefitServiceCollectionExtensions
这个Refit扩展类进行改造。具体来说,我们要在其中注册一个特殊的DelegatingHandler
,它将作为请求管道中的一个重要环节。这个处理器的主要职责是确保每一个通过Refit发出的请求都能携带必要的认证信息。它会自动为请求添加X-Gateway-Signature
签名头,这个签名头是通过特定算法根据时间戳和密钥生成的。不仅如此,为了保持系统中用户上下文的连续性,这个处理器还会负责传递用户相关的信息,比如用户ID、用户名等关键数据。通过将这个DelegatingHandler
整合到Refit的请求管道中,我们可以在不影响现有业务逻辑的情况下,优雅地解决认证问题,使服务间的通信更加安全可靠。
2.1 新建DelegatingHandler
为了实现网关签名验证和用户信息透传的功能,我们需要在SP.Common
类库的Refit
文件夹下创建一个专门的处理器类。这个处理器将作为请求管道中的重要一环,负责处理所有通过Refit发出的HTTP请求。我们将这个类命名为GatewaySignatureHandler
,它继承自DelegatingHandler
基类。这个处理器类将在每个请求发出前自动为其添加必要的签名信息和用户上下文数据,确保下游服务能够正确识别和处理这些请求。让我们看看具体的实现代码:
using System.Net.Http.Headers;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;namespace SP.Common.Refit;/// <summary>
/// 为所有下游 Refit 请求添加网关签名与用户透传头
/// </summary>
public class GatewaySignatureHandler : DelegatingHandler
{private readonly IHttpContextAccessor _httpContextAccessor;private readonly IConfiguration _configuration;public GatewaySignatureHandler(IHttpContextAccessor httpContextAccessor, IConfiguration configuration){_httpContextAccessor = httpContextAccessor;_configuration = configuration;}protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,CancellationToken cancellationToken){// 添加网关签名var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();var secret = _configuration["GatewaySecret"] ?? "SP_Gateway_Secret_2024";var signature = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"{timestamp}.{secret}"));request.Headers.Remove("X-Gateway-Signature");request.Headers.Add("X-Gateway-Signature", signature);// 透传用户信息(若存在)var httpContext = _httpContextAccessor.HttpContext;var user = httpContext?.User;if (user?.Identity?.IsAuthenticated == true){var userId = user.FindFirstValue("UserId");var userName = user.FindFirstValue("UserName");var email = user.FindFirstValue("Email");if (!string.IsNullOrEmpty(userId))request.Headers.TryAddWithoutValidation("X-User-Id", userId);if (!string.IsNullOrEmpty(userName))request.Headers.TryAddWithoutValidation("X-User-Name", userName);if (!string.IsNullOrEmpty(email))request.Headers.TryAddWithoutValidation("X-User-Email", email);}// 如果上游带了 Authorization,则透传(有些服务可能需要)if (httpContext?.Request?.Headers?.TryGetValue("Authorization", out var authHeader) == true){if (!request.Headers.Contains("Authorization")){request.Headers.Authorization = AuthenticationHeaderValue.Parse(authHeader!);}}return await base.SendAsync(request, cancellationToken);}
}
让我们详细解析这段代码的实现。处理器的核心逻辑在重写的SendAsync
方法中实现。首先,它会生成网关签名。签名的生成采用了时间戳和密钥组合的方式,将当前的UTC时间戳与配置中的网关密钥(如果未配置则使用默认值)拼接后进行Base64编码。这个签名会被添加到请求头的X-Gateway-Signature
字段中,用于下游服务的验证。接下来,处理器会处理用户信息的透传。它首先检查当前请求的用户是否已认证,如果已认证,则从用户声明中提取用户ID、用户名和邮箱等信息。这些信息会被分别添加到请求头中,使用X-User-Id
、X-User-Name
和X-User-Email
等自定义头部字段。这样做的目的是确保下游服务能够获知请求的发起者信息。最后,处理器还会检查是否存在授权头(Authorization),如果存在且目标请求还没有设置授权头,则会将其透传到下游请求中。这个功能特别适用于需要保持认证状态的微服务调用场景。
2.2 注册DelegatingHandler
为了使GatewaySignatureHandler
生效,我们需要在Refit的服务注册过程中添加它。具体来说,我们需要在RefitServiceCollectionExtensions
类的AddRefitClient
方法中注册这个处理器。这个处理器会被添加到HTTP请求管道中,作为请求处理的一个重要环节。通过在服务注册时配置这个处理器,我们可以确保所有的服务间通信都会自动携带必要的网关签名和用户信息,从而保证请求能够顺利通过下游服务的验证。让我们看看具体的实现代码:
// more code ...
public static IHttpClientBuilder AddNacosRefitClient<TClient>(this IServiceCollection services,string serviceName,string? groupName,string? clusterName,string scheme = "http",RefitSettings? refitSettings = null)where TClient : class
{services.AddTransient<GatewaySignatureHandler>();return services.AddRefitClient<TClient>(refitSettings ?? new RefitSettings()).ConfigureHttpClient(c => c.BaseAddress = new Uri("http://placeholder")).AddHttpMessageHandler(sp => new NacosDiscoveryHandler(sp.GetRequiredService<IServiceDiscovery>(),serviceName,groupName ?? "DEFAULT_GROUP",clusterName ?? "DEFAULT",scheme,sp.GetRequiredService<ILogger<NacosDiscoveryHandler>>())).AddHttpMessageHandler<GatewaySignatureHandler>();
}
// more code ...
在上面的代码中,我们通过两个关键步骤来注册和使用GatewaySignatureHandler
。首先,services.AddTransient<GatewaySignatureHandler>()
将处理器注册为瞬态服务,这意味着每次需要时都会创建一个新的实例,这样可以确保处理器在处理并发请求时的线程安全性。其次,.AddHttpMessageHandler<GatewaySignatureHandler>()
将处理器添加到HTTP消息处理管道中,使其能够拦截和处理所有出站的HTTP请求。这样,每个通过Refit发出的请求都会经过这个处理器,自动获得必要的网关签名和用户信息,从而确保请求能够顺利通过下游服务的验证。
三、总结
这篇文章详细探讨了在微服务架构中使用Refit进行服务间通信时遇到的未授权访问问题及其解决方案。通过分析,我们发现问题的根源在于Refit直接调用服务实例时绕过了网关,导致缺少必要的网关签名验证信息。为了解决这个问题,我们实现了一个自定义的DelegatingHandler
,它能够自动为所有Refit请求添加网关签名和用户信息。这个处理器不仅确保了请求能够通过下游服务的验证,还实现了用户上下文的透传,使得服务间的通信更加安全可靠。这个解决方案优雅地解决了认证问题,同时保持了代码的整洁性和可维护性,为微服务架构中的服务间通信提供了一个实用的范例。