在前面的文章中,我們會看到使用ContextSession
來獲取當前用戶的UserId
和UserName
。這篇文章我們就一起來看看如何實現ContextSession
。
一、ContextSession的實現
我們在公共類庫SP.Common
中創建一個名為ContextSession
的類,用于獲取當前請求的用戶信息。這個類依賴于IHttpContextAccessor
,它允許我們訪問當前HTTP請求的上下文。
using Microsoft.AspNetCore.Http;namespace SP.Common;/// <summary>
/// 上下文會話(用于保存當前請求的用戶信息)
/// </summary>
public class ContextSession
{private readonly IHttpContextAccessor _httpContextAccessor;public ContextSession(IHttpContextAccessor httpContextAccessor){_httpContextAccessor = httpContextAccessor;}/// <summary>/// 獲取當前請求的用戶ID/// </summary>public long UserId{get{// UserId存儲在Claims中var userIdClaim = _httpContextAccessor.HttpContext?.User?.FindFirst("UserId");if (userIdClaim != null && long.TryParse(userIdClaim.Value, out var userId)){return userId;}return 0;}}/// <summary>/// 獲取當前請求的用戶名/// </summary>public string UserName{get{// UserName存儲在Claims中var userNameClaim = _httpContextAccessor.HttpContext?.User?.FindFirst("UserName");return userNameClaim?.Value ?? string.Empty;}}
}
在這個類中,我們通過IHttpContextAccessor
訪問當前HTTP上下文,并從中獲取用戶的ID和用戶名。用戶信息通常存儲在Claims中,這樣可以方便地在整個應用程序中使用。具體來說,ContextSession
通過讀取HttpContext.User
中的Claims來獲取用戶信息。通常在用戶登錄認證成功后,系統會將用戶的相關信息(如UserId、UserName等)以Claim的形式添加到ClaimsPrincipal
中。這樣,在后續的每個HTTP請求中,都可以通過ContextSession
統一獲取當前用戶的身份信息,無需在各個業務模塊中重復解析。
這種做法有以下優點:
- 解耦業務邏輯與認證實現:業務代碼無需關心認證的具體實現細節,只需通過
ContextSession
獲取用戶信息即可。 - 便于測試和維護:通過依賴注入
IHttpContextAccessor
,可以方便地進行單元測試和Mock。 - 統一管理用戶上下文:所有需要獲取當前用戶信息的地方都可以通過
ContextSession
訪問,避免了代碼重復。
需要注意的是,IHttpContextAccessor
需要在Program
中注冊為服務,作用域設置為Scoped
,否則在依賴注入時會報錯。注冊代碼如下:
// 在Program.cs中添加
// 注冊 IHttpContextAccessor
builder.Services.AddHttpContextAccessor();
// 注冊 ContextSession
builder.Services.AddScoped<ContextSession>();
這樣就可以在需要的地方通過依賴注入獲取ContextSession
實例,進而獲取當前用戶的相關信息。
二、寫入UserId和UserName到Claims中
在用戶登錄認證成功后,我們需要將用戶的UserId
和UserName
寫入到Claims中,以便后續請求可以通過ContextSession
獲取。通常在認證過程中,我們需要編寫通用中間件來處理用戶登錄后將用戶信息添加到Claims中。
我們在SP.Common
中的Middleware
文件夾下創建一個名為ApplicationMiddleware
的中間件,它是一個通用的中間件,用于處理用戶登錄認證和將用戶信息寫入Claims,所有業務微服務都要引用這個中間件。代碼如下:
using Microsoft.AspNetCore.Http;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using SP.Common.ExceptionHandling.Exceptions;
using SP.Common.ConfigService;namespace SP.Common.Middleware;/// <summary>
/// 應用程序中間件,所有微服務都要引入
/// </summary>
public class ApplicationMiddleware
{private readonly RequestDelegate _next;private readonly JwtConfigService _jwtConfigService;/// <summary>/// 應用程序中間件構造函數/// </summary>/// <param name="next">下一個中間件</param>/// <param name="jwtConfigService">Jwt配置服務</param>public ApplicationMiddleware(RequestDelegate next, JwtConfigService jwtConfigService){_next = next;_jwtConfigService = jwtConfigService;}/// <summary>/// 中間件處理請求/// </summary>/// <param name="context">HTTP上下文</param>/// <returns>異步任務</returns>public async Task InvokeAsync(HttpContext context){// 1. 獲取Authorization頭var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)){var token = authHeader.Substring("Bearer ".Length).Trim();try{var handler = new JwtSecurityTokenHandler();var jwtToken = handler.ReadJwtToken(token);var claims = jwtToken.Claims.ToList();// 查找UserId和UserNamevar userId = claims.FirstOrDefault(c => c.Type == "UserId")?.Value;var userName = claims.FirstOrDefault(c => c.Type == "UserName")?.Value;// 如果HttpContext.User沒有身份,則新建if (context.User == null || !context.User.Identities.Any()){var identity = new ClaimsIdentity(claims, "jwt");context.User = new ClaimsPrincipal(identity);}else{// 合并claims到現有identityvar identity = context.User.Identities.First();if (!string.IsNullOrEmpty(userId) && !identity.HasClaim(c => c.Type == "UserId"))identity.AddClaim(new Claim("UserId", userId));if (!string.IsNullOrEmpty(userName) && !identity.HasClaim(c => c.Type == "UserName"))identity.AddClaim(new Claim("UserName", userName));}}catch{throw new UnauthorizedException("用戶未登錄");}}// 調用下一個中間件await _next(context);}
}
在這個中間件的實現過程中,首先會對每一個進入的HTTP請求檢查其請求頭中的Authorization
字段,判斷其是否存在且以Bearer
作為前綴。如果滿足條件,則從該字段中提取出JWT令牌,并利用JwtSecurityTokenHandler
對令牌進行解析。解析后,可以從JWT的Claims中獲取到用戶的UserId
和UserName
等關鍵信息。接下來,中間件會判斷當前HttpContext.User
是否已經包含身份信息。如果沒有身份信息,則會新建一個ClaimsIdentity
,并將解析得到的Claims(包括UserId
和UserName
)添加進去,隨后將其賦值給HttpContext.User
。如果已經存在身份信息,則會檢查當前身份中是否已經包含了UserId
和UserName
這兩個Claim,如果沒有,則將其補充進去。通過這種方式,無論請求是否已經有身份信息,都能確保UserId
和UserName
被正確地寫入到Claims中。這樣一來,后續的業務處理中,只需要通過ContextSession
即可方便地獲取當前用戶的身份信息,實現了用戶上下文的統一管理和解耦,極大地方便了微服務架構下的用戶認證與授權流程。
三、總結
在這篇文章中,我們實現了ContextSession
類,用于獲取當前請求的用戶信息,并通過IHttpContextAccessor
訪問HTTP上下文。同時,我們創建了一個通用的中間件ApplicationMiddleware
,用于處理用戶登錄認證和將用戶信息寫入Claims中。這樣,我們就可以在微服務架構中統一管理用戶上下文,方便地獲取當前用戶的身份信息。
這種設計模式不僅提高了代碼的可維護性和可讀性,還使得用戶認證和授權的邏輯更加清晰和集中。通過將用戶信息存儲在Claims中,我們可以在整個應用程序中輕松訪問用戶的身份信息,而無需在每個業務模塊中重復解析。也為后續的擴展和修改提供了便利,使得在需要添加新的用戶信息或修改現有邏輯時,只需在ContextSession
或中間件中進行調整即可,而不需要在每個微服務中都進行修改。它在微服務架構中尤為重要,因為它允許我們在多個服務之間共享用戶信息,同時保持服務的獨立性和解耦性。通過這種方式,我們可以更好地管理用戶身份,確保系統的安全性和一致性。這種方法的實現也展示了ASP.NET Core中間件和依賴注入的強大功能,使得我們可以輕松地在應用程序中實現復雜的用戶認證和授權邏輯,而無需編寫大量重復的代碼。