using Dpz.Core.Service.Mediator.Features.Account.Queries;

namespace Dpz.Core.Web.Library.Middleware;

/// <summary>
/// 用户资料刷新中间件
/// 每次请求时检查当前登录用户的资料是否被标记为已变更(通过 FusionCache 存储的变更时间戳),
/// 若资料已过期则重新查询用户信息并刷新登录态
/// </summary>
public class UserProfileRefreshMiddleware(
    RequestDelegate next,
    IFusionCache fusionCache,
    ILogger<UserProfileRefreshMiddleware> logger
)
{
    public async Task InvokeAsync(
        HttpContext context,
        IMediator mediator,
        IApplicationSignInManager signInManager
    )
    {
        if (context.User.Authenticated)
        {
            await RefreshProfileIfNeededAsync(context, mediator, signInManager);
        }

        await next(context);
    }

    /// <summary>
    /// 检查用户资料是否需要刷新并执行刷新
    /// 比较缓存中的资料变更时间与登录态中记录的最后刷新时间,
    /// 若变更时间晚于刷新时间则重新查询用户信息并更新登录态
    /// </summary>
    private async Task RefreshProfileIfNeededAsync(
        HttpContext context,
        IMediator mediator,
        IApplicationSignInManager signInManager
    )
    {
        var account = context.User.NameIdentifier;
        var sessionId = context.User.SessionId;
        if (string.IsNullOrWhiteSpace(account) || string.IsNullOrWhiteSpace(sessionId))
        {
            return;
        }

        // 从 FusionCache 读取用户资料变更时间戳
        var changedAt = await fusionCache.GetOrDefaultAsync<DateTime>(
            UserSessionKeys.ChangedAtKey(account),
            token: context.RequestAborted
        );
        if (changedAt == default)
        {
            return;
        }

        // 当前登录态中的最后刷新时间已不早于变更时间,无需刷新
        var refreshedAt = ReadProfileRefreshedAt(context);
        if (refreshedAt >= changedAt)
        {
            return;
        }

        // 重新查询用户最新信息
        var userInfo = await mediator.Send(
            new GetUserInfoRequest { Account = account },
            context.RequestAborted
        );
        if (userInfo == null)
        {
            logger.LogWarning(
                "User profile refresh skipped because account was not found: {Account}",
                account
            );
            return;
        }

        // 更新登录态中的用户信息
        userInfo.SessionId = sessionId;
        await signInManager.RefreshSignInAsync(userInfo);
    }

    /// <summary>
    /// 从当前用户 Claims 中读取最近一次资料刷新的时间
    /// </summary>
    private static DateTime ReadProfileRefreshedAt(HttpContext context)
    {
        var raw = context.User.FindFirst(UserSessionKeys.ProfileRefreshedAtClaim)?.Value;
        if (
            DateTime.TryParse(
                raw,
                CultureInfo.InvariantCulture,
                DateTimeStyles.RoundtripKind,
                out var refreshedAt
            )
        )
        {
            return refreshedAt;
        }

        return default;
    }
}

/// <summary>
/// 用户资料刷新中间件的注册扩展
/// </summary>
public static class UserProfileRefreshMiddlewareExtensions
{
    public static IApplicationBuilder UseUserProfileRefresh(this IApplicationBuilder app)
    {
        return app.UseMiddleware<UserProfileRefreshMiddleware>();
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个 ASP.NET Core 中间件,用于在每次 HTTP 请求时自动检查并刷新已登录用户的资料信息。

核心功能

1. UserProfileRefreshMiddleware 中间件类

构造函数参数:

  • RequestDelegate next: 管道中的下一个中间件
  • IFusionCache fusionCache: 分布式缓存服务
  • ILogger: 日志记录器

主方法 InvokeAsync

public async Task InvokeAsync(HttpContext context, IMediator mediator, IApplicationSignInManager signInManager)
  • 检查当前用户是否已认证(context.User.Authenticated
  • 如果已认证,调用刷新逻辑
  • 继续执行管道中的下一个中间件

2. 刷新逻辑 RefreshProfileIfNeededAsync

工作流程:

  1. 获取用户标识

    • 从当前用户 Claims 中读取账号(NameIdentifier)和会话ID(SessionId
    • 如果任一为空则跳过刷新
  2. 检查资料变更时间

    • FusionCache 中读取用户资料的变更时间戳
    • 缓存键格式:UserSessionKeys.ChangedAtKey(account)
    • 如果没有变更时间记录则跳过
  3. 比较时间戳

    • 从用户 Claims 中读取上次刷新时间(ProfileRefreshedAtClaim
    • 如果刷新时间 >= 变更时间,说明资料是最新的,无需刷新
  4. 重新查询并更新

    • 通过 Mediator 发送 GetUserInfoRequest 查询最新用户信息
    • 如果用户不存在,记录警告日志并跳过
    • 保留原会话ID,调用 signInManager.RefreshSignInAsync 更新登录态

3. 辅助方法 ReadProfileRefreshedAt

  • 从当前用户的 Claims 中读取 ProfileRefreshedAtClaim 的值
  • 使用 InvariantCultureRoundtripKind 解析 DateTime
  • 解析失败则返回默认值(default(DateTime)

4. 扩展方法

public static IApplicationBuilder UseUserProfileRefresh(this IApplicationBuilder app)

提供便捷的中间件注册方式,在 Startup.csProgram.cs 中可以这样使用:

app.UseUserProfileRefresh();

应用场景

当用户资料在其他地方被修改(如管理员修改用户权限、用户更新个人信息等)时:

  1. 修改操作会在 FusionCache 中更新 ChangedAtKey 的时间戳
  2. 用户下次请求时,中间件检测到变更
  3. 自动重新加载用户信息,更新 Cookie/JWT 中的 Claims
  4. 确保用户在当前会话中使用最新的资料和权限

设计优点

性能优化:只在资料变更时才刷新,避免每次请求都查询数据库
分布式友好:使用 FusionCache 支持多服务器场景
解耦设计:通过 Mediator 模式调用查询逻辑
可扩展性:清晰的职责分离,易于维护和测试

评论加载中...