我們在項目中集成了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請求添加網關簽名和用戶信息。這個處理器不僅確保了請求能夠通過下游服務的驗證,還實現了用戶上下文的透傳,使得服務間的通信更加安全可靠。這個解決方案優雅地解決了認證問題,同時保持了代碼的整潔性和可維護性,為微服務架構中的服務間通信提供了一個實用的范例。