using Dpz.Core.Service.Mediator.Features.Security.Queries;
using Microsoft.AspNetCore.Authentication;

namespace Dpz.Core.Auth.Middleware;

/// <summary>
/// SecurityStamp验证中间件
/// 用于在密码修改后自动登出其他浏览器的会话
/// </summary>
public class SecurityStampValidationMiddleware(
    RequestDelegate next,
    ILogger<SecurityStampValidationMiddleware> logger
)
{
    public async Task InvokeAsync(
        HttpContext context,
        IMediator mediator,
        IAccountSessionService accountSessionService
    )
    {
        // 只对已认证的用户进行验证
        if (context.User.Identity?.IsAuthenticated == true)
        {
            var account = context.User.NameIdentifier;
            var claimKey = context.User.FindFirst("Key")?.Value;
            var sessionId = context.User.SessionId;

            if (
                !string.IsNullOrWhiteSpace(sessionId)
                && await accountSessionService.IsRevokedAsync(sessionId, context.RequestAborted)
            )
            {
                logger.LogInformation(
                    "用户 {Account} 的会话 {SessionId} 已撤销",
                    account,
                    sessionId
                );
                await context.SignOutAsync();
                context.Response.Redirect("/auth-status/session-expired");
                return;
            }

            if (!string.IsNullOrEmpty(account) && !string.IsNullOrEmpty(claimKey))
            {
                try
                {
                    var result = await mediator.Send(
                        new ValidateUserSecurityStampRequest
                        {
                            UserId = account,
                            SecurityStamp = claimKey,
                        }
                    );
                    var validationResult = result.Success
                        ? result.Data
                        : UserSecurityValidationResult.SecurityStampInvalid;

                    // 根据不同的验证结果进行处理
                    if (validationResult != UserSecurityValidationResult.Valid)
                    {
                        logger.LogInformation(
                            "用户 {Account} 的验证失败,原因:{Reason}",
                            account,
                            validationResult
                        );

                        // 登出用户
                        await context.SignOutAsync();

                        // 根据不同的失败原因,重定向到不同的错误页面
                        var redirectUrl = validationResult switch
                        {
                            UserSecurityValidationResult.UserDisabled =>
                                "/auth-status/account-disabled",
                            UserSecurityValidationResult.SecurityStampInvalid =>
                                "/auth-status/session-expired",
                            UserSecurityValidationResult.UserNotFound =>
                                "/auth-status/user-not-found",
                            _ => "/auth-status/auth-error",
                        };

                        context.Response.Redirect(redirectUrl);
                        return;
                    }
                }
                catch (Exception ex)
                {
                    logger.LogError(ex, "验证用户 {Account} 的SecurityStamp时发生错误", account);
                }
            }
        }

        await next(context);
    }
}

/// <summary>
/// SecurityStamp验证中间件扩展方法
/// </summary>
public static class SecurityStampValidationMiddlewareExtensions
{
    public static IApplicationBuilder UseSecurityStampValidation(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<SecurityStampValidationMiddleware>();
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个 ASP.NET Core 中间件,用于实现基于 SecurityStamp(安全戳)的会话验证机制。主要功能是在用户修改密码或其他敏感操作后,自动使其他浏览器/设备上的会话失效。

核心组件

1. SecurityStampValidationMiddleware 中间件类

使用了 主构造函数语法(C# 12)接收依赖注入:

  • RequestDelegate next:管道中的下一个中间件
  • ILogger<SecurityStampValidationMiddleware> logger:日志记录器

2. InvokeAsync 方法 - 中间件核心逻辑

public async Task InvokeAsync(
    HttpContext context,
    IMediator mediator,  // 中介者模式,用于发送查询请求
    IAccountSessionService accountSessionService  // 会话服务
)

执行流程:

步骤 1:身份验证检查

if (context.User.Identity?.IsAuthenticated == true)

仅对已登录用户进行验证。

步骤 2:提取用户信息

var account = context.User.NameIdentifier;      // 用户标识
var claimKey = context.User.FindFirst("Key")?.Value;  // SecurityStamp
var sessionId = context.User.SessionId;          // 会话ID

步骤 3:会话撤销检查

if (await accountSessionService.IsRevokedAsync(sessionId, ...))
{
    await context.SignOutAsync();  // 登出
    context.Response.Redirect("/auth-status/session-expired");
    return;
}

检查会话是否被主动撤销(如管理员强制登出)。

步骤 4:SecurityStamp 验证

var result = await mediator.Send(
    new ValidateUserSecurityStampRequest
    {
        UserId = account,
        SecurityStamp = claimKey,
    }
);

通过 MediatR 发送验证请求,检查用户的 SecurityStamp 是否仍然有效。

步骤 5:处理验证结果

if (validationResult != UserSecurityValidationResult.Valid)
{
    await context.SignOutAsync();  // 登出
    
    // 根据失败原因重定向
    var redirectUrl = validationResult switch
    {
        UserSecurityValidationResult.UserDisabled => "/auth-status/account-disabled",
        UserSecurityValidationResult.SecurityStampInvalid => "/auth-status/session-expired",
        UserSecurityValidationResult.UserNotFound => "/auth-status/user-not-found",
        _ => "/auth-status/auth-error",
    };
    
    context.Response.Redirect(redirectUrl);
    return;
}

可能的验证失败原因:

  • 账户被禁用
  • SecurityStamp 不匹配(密码已修改)
  • 用户不存在

步骤 6:继续管道

await next(context);

验证通过后,调用下一个中间件。

3. 扩展方法

public static IApplicationBuilder UseSecurityStampValidation(this IApplicationBuilder builder)
{
    return builder.UseMiddleware<SecurityStampValidationMiddleware>();
}

提供了便捷的注册方式,在 Program.csStartup.cs 中使用:

app.UseSecurityStampValidation();

应用场景

  1. 密码修改后自动登出:用户在设备 A 修改密码后,设备 B 的会话自动失效
  2. 强制登出:管理员可以撤销特定会话
  3. 账户安全保护:账户被禁用时立即终止所有会话
  4. 分布式会话管理:配合 IAccountSessionService 实现集中式会话控制

技术亮点

  • ✅ 使用 MediatR 实现解耦的查询处理
  • ✅ 支持 细粒度的会话管理(SessionId 级别)
  • ✅ 提供 友好的错误页面重定向
  • ✅ 完善的 日志记录
  • 异步处理,性能友好

这是一个生产级的安全中间件实现,常用于需要高安全性的企业应用系统。

评论加载中...