using System.Security.Claims;
using Dpz.Core.Service.Mediator.Features.Security.Queries;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;

namespace Dpz.Core.Auth.Service;

public sealed class OidcSignInManager(
    UserManager<VmUserInfo> userManager,
    IHttpContextAccessor contextAccessor,
    IUserClaimsPrincipalFactory<VmUserInfo> claimsFactory,
    IOptions<IdentityOptions> optionsAccessor,
    ILoggerFactory loggerFactory,
    IAuthenticationSchemeProvider schemes,
    IUserConfirmation<VmUserInfo> confirmation,
    IMediator mediator,
    IFusionCache fusionCache,
    IAccountSessionService accountSessionService
)
    : SignInManager<VmUserInfo>(
        userManager,
        contextAccessor,
        claimsFactory,
        optionsAccessor,
        loggerFactory.CreateLogger<SignInManager<VmUserInfo>>(),
        schemes,
        confirmation
    )
{
    public override async Task SignInAsync(
        VmUserInfo user,
        AuthenticationProperties authenticationProperties,
        string? authenticationMethod = null
    )
    {
        InitSessionId(user);
        var userPrincipalAsync = await CreateUserPrincipalAsync(user);
        if (authenticationMethod != null)
        {
            userPrincipalAsync
                .Identities.First()
                .AddClaim(
                    new Claim(
                        "https://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod",
                        authenticationMethod
                    )
                );
        }
        await CacheUserSessionAsync(user);
        await Context.SignInAsync(
            ConstValues.DefaultScheme,
            userPrincipalAsync,
            authenticationProperties
        );
    }

    private readonly IUserClaimsPrincipalFactory<VmUserInfo> _claimsFactory = claimsFactory;
    private readonly ILogger<SignInManager<VmUserInfo>> _logger =
        loggerFactory.CreateLogger<OidcSignInManager>();

    public override async Task<SignInResult> PasswordSignInAsync(
        string userName,
        string password,
        bool isPersistent,
        bool lockoutOnFailure
    )
    {
        var verifyResult = await mediator.Send(
            new VerifyAccountPasswordRequest { Account = userName, Password = password }
        );
        var userInfo = verifyResult.Data;

        // 检查用户是否存在且密码是否正确
        if (!verifyResult.Success || userInfo == null)
        {
            return SignInResult.Failed;
        }

        // 检查用户是否被禁用
        if (userInfo.Enable == false)
        {
            _logger.LogWarning("用户 {UserName} 被禁用,登录被拒绝", userName);
            // 设置标记以表示账户被禁用
            Context.SetAccountDisabled();
            return SignInResult.Failed;
        }

        var result = await SignInOrTwoFactorAsync(userInfo, isPersistent);
        return result;
    }

    public override async Task RefreshSignInAsync(VmUserInfo user)
    {
        var authenticateResult = await Context.AuthenticateAsync();
        var sessionId = authenticateResult.Principal.SessionId;
        if (!string.IsNullOrWhiteSpace(sessionId))
        {
            user.SessionId = sessionId;
        }
        await CacheUserSessionAsync(user);
        await Context.SignInAsync(
            ConstValues.DefaultScheme,
            await _claimsFactory.CreateAsync(user),
            authenticateResult.Properties
        );
    }

    /// <summary>
    /// 判断应用程序cookie身份
    /// </summary>
    public override bool IsSignedIn(ClaimsPrincipal principal)
    {
        _logger.LogInformation("IsSignedIn");
        if (principal == null)
        {
            throw new ArgumentNullException(nameof(principal));
        }

        return principal.Identities.Any(i => i.AuthenticationType == ConstValues.DefaultScheme);
    }

    /// <summary>
    /// 登录指定的用户 <see cref="VmUserInfo"/>
    /// </summary>
    public override async Task SignInWithClaimsAsync(
        VmUserInfo user,
        AuthenticationProperties? authenticationProperties,
        IEnumerable<Claim> additionalClaims
    )
    {
        InitSessionId(user);
        var userPrincipal = await CreateUserPrincipalAsync(user);
        foreach (var claim in additionalClaims)
        {
            userPrincipal.Identities.First().AddClaim(claim);
        }

        await CacheUserSessionAsync(user);
        await Context.SignInAsync(
            ConstValues.DefaultScheme,
            userPrincipal,
            authenticationProperties ?? new AuthenticationProperties()
        );
    }

    private async Task CacheUserSessionAsync(VmUserInfo user)
    {
        InitSessionId(user);
        var sessionId = user.SessionId!;

        await fusionCache.SetAsync(UserSessionKeys.InfoKey(sessionId), user, TimeSpan.FromDays(30));
        await accountSessionService.CreateOrRefreshAsync(user);
    }

    private static void InitSessionId(VmUserInfo user)
    {
        if (string.IsNullOrWhiteSpace(user.SessionId))
        {
            user.SessionId = Guid.NewGuid().ToString("N");
        }
    }
}

/// <summary>
/// SignInResult 扩展方法,用于标记不同的失败原因
/// </summary>
public static class SignInResultExtensions
{
    private const string AccountDisabledKey = "AccountDisabled";

    /// <summary>
    /// 设置账户被禁用标记到 HttpContext
    /// </summary>
    public static void SetAccountDisabled(this HttpContext context)
    {
        context.Items[AccountDisabledKey] = true;
    }

    /// <summary>
    /// 检查 HttpContext 中是否有账户被禁用标记
    /// </summary>
    public static bool HasAccountDisabledFlag(this HttpContext context)
    {
        return context.Items.ContainsKey(AccountDisabledKey);
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个自定义的 OIDC(OpenID Connect)登录管理器实现,继承自 ASP.NET Core Identity 的 SignInManager<VmUserInfo>。主要用于处理用户身份验证、登录和会话管理。

主要组件

OidcSignInManager 类

这是核心类,通过主构造函数注入了多个依赖项:

  • UserManager:用户管理器
  • IHttpContextAccessor:HTTP 上下文访问器
  • IMediator:中介者模式实现(用于发送命令/查询)
  • IFusionCache:缓存服务
  • IAccountSessionService:账户会话服务

核心方法解析

1. SignInAsync - 用户登录

public override async Task SignInAsync(...)
  • 初始化会话 ID
  • 创建用户主体(ClaimsPrincipal)
  • 可选添加认证方法声明
  • 缓存用户会话
  • 执行实际的登录操作

2. PasswordSignInAsync - 密码登录

public override async Task<SignInResult> PasswordSignInAsync(...)
  • 通过 Mediator 验证用户名和密码
  • 检查账户是否存在
  • 关键业务逻辑:检查账户是否被禁用
    • 如果禁用,记录日志并在 HttpContext 中设置禁用标记
    • 返回登录失败结果
  • 成功则执行登录或双因素认证流程

3. RefreshSignInAsync - 刷新登录状态

public override async Task RefreshSignInAsync(VmUserInfo user)
  • 获取当前认证结果和会话 ID
  • 更新用户会话缓存
  • 重新签发认证 Cookie

4. IsSignedIn - 检查用户是否已登录

public override bool IsSignedIn(ClaimsPrincipal principal)
  • 验证用户主体的认证类型是否匹配默认方案

5. SignInWithClaimsAsync - 带额外声明的登录

public override async Task SignInWithClaimsAsync(...)
  • 支持添加额外的用户声明(Claims)
  • 适用于需要自定义用户属性的场景

私有辅助方法

CacheUserSessionAsync

  • 缓存用户会话信息到 FusionCache(30天有效期)
  • 通过 IAccountSessionService 创建或刷新会话记录

InitSessionId

  • 如果会话 ID 为空,生成新的 GUID 作为会话标识

SignInResultExtensions 扩展类

提供了两个静态扩展方法:

SetAccountDisabled

HttpContext.Items 中设置账户禁用标记,用于跨中间件传递状态信息。

HasAccountDisabledFlag

检查 HttpContext 是否包含账户禁用标记,可用于后续中间件或过滤器中判断失败原因。

设计亮点

  1. 会话管理:双重存储(缓存 + 服务层),确保会话持久性
  2. 安全增强:检查账户启用状态,防止禁用账户登录
  3. 可扩展性:通过 Mediator 模式解耦验证逻辑
  4. 状态传递:使用 HttpContext.Items 传递特定失败原因
  5. 日志记录:关键操作都有日志追踪

使用场景

适用于需要 OIDC 标准兼容的身份验证系统,支持:

  • 标准用户名密码登录
  • 会话持久化
  • 账户状态控制(启用/禁用)
  • 自定义声明扩展
评论加载中...