在前面的文章中,我们会看到使用ContextSession
来获取当前用户的UserId
和UserName
。这篇文章我们就一起来看看如何实现ContextSession
。
一、ContextSession的实现
我们在公共类库SP.Common
中创建一个名为ContextSession
的类,用于获取当前请求的用户信息。这个类依赖于IHttpContextAccessor
,它允许我们访问当前HTTP请求的上下文。
csharp
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
,否则在依赖注入时会报错。注册代码如下:
csharp
// 在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,所有业务微服务都要引用这个中间件。代码如下:
csharp
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和UserName
var 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到现有identity
var 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中间件和依赖注入的强大功能,使得我们可以轻松地在应用程序中实现复杂的用户认证和授权逻辑,而无需编写大量重复的代码。