30.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--公共代码--用户上下文会话

在前面的文章中,我们会看到使用ContextSession来获取当前用户的UserIdUserName。这篇文章我们就一起来看看如何实现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中

在用户登录认证成功后,我们需要将用户的UserIdUserName写入到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中获取到用户的UserIdUserName等关键信息。接下来,中间件会判断当前HttpContext.User是否已经包含身份信息。如果没有身份信息,则会新建一个ClaimsIdentity,并将解析得到的Claims(包括UserIdUserName)添加进去,随后将其赋值给HttpContext.User。如果已经存在身份信息,则会检查当前身份中是否已经包含了UserIdUserName这两个Claim,如果没有,则将其补充进去。通过这种方式,无论请求是否已经有身份信息,都能确保UserIdUserName被正确地写入到Claims中。这样一来,后续的业务处理中,只需要通过ContextSession即可方便地获取当前用户的身份信息,实现了用户上下文的统一管理和解耦,极大地方便了微服务架构下的用户认证与授权流程。

三、总结

在这篇文章中,我们实现了ContextSession类,用于获取当前请求的用户信息,并通过IHttpContextAccessor访问HTTP上下文。同时,我们创建了一个通用的中间件ApplicationMiddleware,用于处理用户登录认证和将用户信息写入Claims中。这样,我们就可以在微服务架构中统一管理用户上下文,方便地获取当前用户的身份信息。

这种设计模式不仅提高了代码的可维护性和可读性,还使得用户认证和授权的逻辑更加清晰和集中。通过将用户信息存储在Claims中,我们可以在整个应用程序中轻松访问用户的身份信息,而无需在每个业务模块中重复解析。也为后续的扩展和修改提供了便利,使得在需要添加新的用户信息或修改现有逻辑时,只需在ContextSession或中间件中进行调整即可,而不需要在每个微服务中都进行修改。它在微服务架构中尤为重要,因为它允许我们在多个服务之间共享用户信息,同时保持服务的独立性和解耦性。通过这种方式,我们可以更好地管理用户身份,确保系统的安全性和一致性。这种方法的实现也展示了ASP.NET Core中间件和依赖注入的强大功能,使得我们可以轻松地在应用程序中实现复杂的用户认证和授权逻辑,而无需编写大量重复的代码。

相关推荐
sivdead34 分钟前
从被动查询到主动智能:数据应用智能体的技术演进路线图
人工智能·后端·架构
拳打南山敬老院1 小时前
从零构建一个插件系统(四)插件的缓存
javascript·架构
时光追逐者1 小时前
C#/.NET/.NET Core技术前沿周刊 | 第 48 期(2025年7.21-7.27)
c#·.net·.netcore·.net core
蓝点lilac2 小时前
C# 调用邮箱应用发送带附件的邮件
c#·.net
love530love3 小时前
Windows 11 下 Anaconda 命令修复指南及常见问题解决
运维·ide·人工智能·windows·python·架构·conda
java叶新东老师3 小时前
七、搭建springCloudAlibaba2021.1版本分布式微服务-skywalking9.0链路追踪
分布式·微服务·架构
Dolphin_海豚3 小时前
前端工程化总览
前端·架构·前端工程化
不甘打工的程序猿4 小时前
nacos融合spring cloud学习【Spring-Cloud-Alibaba】
后端·架构
ArabySide5 小时前
【Linux】Ubuntu上安装.NET 9运行时与ASP.NET Core项目部署入门
linux·ubuntu·.net
小乖兽技术5 小时前
在 .NET 中使用 Base64 时容易踩的坑总结
开发语言·c#·.net