namespace Dpz.Core.Web.Library;

public class UserStore(IAccountService accountService) : IUserStore<VmUserInfo>
{
    public void Dispose() { }

    public async Task<string> GetUserIdAsync(VmUserInfo user, CancellationToken cancellationToken)
    {
        return await Task.Run(() => user.Id, cancellationToken);
    }

    public async Task<string?> GetUserNameAsync(
        VmUserInfo user,
        CancellationToken cancellationToken
    )
    {
        return await Task.Run(() => user.Name, cancellationToken);
    }

    public async Task SetUserNameAsync(
        VmUserInfo user,
        string? userName,
        CancellationToken cancellationToken
    )
    {
        ArgumentNullException.ThrowIfNull(userName);
        await Task.Run(() => user.Name = userName, cancellationToken);
    }

    public async Task<string?> GetNormalizedUserNameAsync(
        VmUserInfo user,
        CancellationToken cancellationToken
    )
    {
        return await Task.Run(() => user.Name, cancellationToken);
    }

    public async Task SetNormalizedUserNameAsync(
        VmUserInfo user,
        string? normalizedName,
        CancellationToken cancellationToken
    )
    {
        ArgumentNullException.ThrowIfNull(normalizedName);
        await Task.Run(() => user.Name = normalizedName, cancellationToken);
    }

    public Task<IdentityResult> CreateAsync(VmUserInfo user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<IdentityResult> UpdateAsync(VmUserInfo user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public Task<IdentityResult> DeleteAsync(VmUserInfo user, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    public async Task<VmUserInfo?> FindByIdAsync(string userId, CancellationToken cancellationToken)
    {
        return await accountService.GetOneUserAsync(userId, cancellationToken);
    }

    public async Task<VmUserInfo?> FindByNameAsync(
        string normalizedUserName,
        CancellationToken cancellationToken
    )
    {
        return await accountService.GetUserInfoByNameAsync(normalizedUserName);
    }
}

public class UserInfoClaimsPrincipalFactory : IUserClaimsPrincipalFactory<VmUserInfo>
{
    public Task<ClaimsPrincipal> CreateAsync(VmUserInfo user)
    {
        var claims = user.GetType()
            .GetProperties()
            .Select(x => new Claim(x.Name, x.GetValue(user)?.ToString() ?? ""))
            .ToList();
        claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id));
        var principal = new ClaimsPrincipal(
            new ClaimsIdentity(claims, Program.AuthorizeCookieName, "Name", "Permissions")
        );
        return Task.FromResult(principal);
    }
}

public interface IApplicationSignInManager
{
    Task SignInAsync(
        VmUserInfo user,
        AuthenticationProperties authenticationProperties,
        string? authenticationMethod = null
    );

    Task RefreshSignInAsync(VmUserInfo user);
}

public class ApplicationSignInManager(
    UserManager<VmUserInfo> userManager,
    IHttpContextAccessor contextAccessor,
    IUserClaimsPrincipalFactory<VmUserInfo> claimsFactory,
    IOptions<IdentityOptions> optionsAccessor,
    ILogger<ApplicationSignInManager> logger,
    IAuthenticationSchemeProvider schemes,
    IUserConfirmation<VmUserInfo> userConfirmation,
    IFusionCache fusionCache
)
    : SignInManager<VmUserInfo>(
        userManager,
        contextAccessor,
        claimsFactory,
        optionsAccessor,
        logger,
        schemes,
        userConfirmation
    ),
        IApplicationSignInManager
{
    private readonly IUserClaimsPrincipalFactory<VmUserInfo> _claimsFactory = claimsFactory;

    public override async Task SignInAsync(
        VmUserInfo user,
        AuthenticationProperties authenticationProperties,
        string? authenticationMethod = null
    )
    {
        var userPrincipalAsync = await base.CreateUserPrincipalAsync(user);
        if (authenticationMethod != null)
        {
            userPrincipalAsync
                .Identities.First()
                .AddClaim(
                    new Claim(
                        "https://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod",
                        authenticationMethod
                    )
                );
        }

        await Context.SignInAsync(
            Program.AuthorizeCookieName,
            userPrincipalAsync,
            authenticationProperties
        );

        await fusionCache.SetAsync(_userInfoKey(user.Id), user, TimeSpan.FromDays(14));
    }

    private readonly Func<string, string> _userInfoKey = x => $"account-info-{x}";

    public override async Task RefreshSignInAsync(VmUserInfo user)
    {
        var authenticateResult = await Context.AuthenticateAsync(Program.AuthorizeCookieName);

        // 生成最新的 ClaimsPrincipal
        var newPrincipal = await _claimsFactory.CreateAsync(user);

        if (authenticateResult.Principal != null)
        {
            var oldAuthMethod = authenticateResult
                .Principal.FindFirst(
                    "https://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod"
                )
                ?.Value;
            if (
                !string.IsNullOrEmpty(oldAuthMethod)
                && newPrincipal
                    .Identities.FirstOrDefault()
                    ?.FindFirst(
                        "https://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod"
                    ) == null
            )
            {
                newPrincipal
                    .Identities.First()
                    .AddClaim(
                        new Claim(
                            "https://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod",
                            oldAuthMethod
                        )
                    );
            }
        }

        var props = authenticateResult.Properties ?? new AuthenticationProperties();

        await Context.SignInAsync(Program.AuthorizeCookieName, newPrincipal, props);
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

下面按模块逐一解释这段代码的功能、行为和一些值得注意的问题及改进建议。

总体概述

  • 这是一个基于 ASP.NET Core Identity 的定制实现:
    • UserStore: 为 Identity 提供用户持久化/查询的最小实现(对 VmUserInfo)。
    • UserInfoClaimsPrincipalFactory: 将 VmUserInfo 转换为 ClaimsPrincipal(用于生成 cookie 等)。
    • ApplicationSignInManager: 对 SignInManager 的扩展,负责登录(写 cookie)并把用户信息缓存在 IFusionCache 中;还实现了刷新登录(RefreshSignIn)时替换 Principal 的逻辑。
  • 代码通过依赖注入接入了 IAccountService、IFusionCache 等外部服务。

逐类说明

  1. UserStore : IUserStore
  • 构造参数:接收 IAccountService(代码顶部用 record-like ctor 语法注入)。
  • 已实现的方法:
    • Dispose(): 空实现。
    • GetUserIdAsync / GetUserNameAsync / SetUserNameAsync / GetNormalizedUserNameAsync / SetNormalizedUserNameAsync:
      • 这些方法都用 Task.Run 包装同步属性访问/赋值来返回异步结果。
      • SetUserNameAsync/SetNormalizedUserNameAsync 使用 ArgumentNullException.ThrowIfNull 检查入参(注意参数签名允许 string?,但会抛异常)。
      • GetNormalizedUserNameAsync/SetNormalizedUserNameAsync 实际上都直接读/写 user.Name(没有额外的 NormalizedName 字段),即把“规范化用户名”当作与 Name 相同来处理。
    • FindByIdAsync 使用 accountService.GetOneUserAsync(userId, cancellationToken) 查询用户。
    • FindByNameAsync 使用 accountService.GetUserInfoByNameAsync(normalizedUserName) 查询用户(没有传 cancellationToken)。
  • 未实现的方法(抛 NotImplementedException):
    • CreateAsync / UpdateAsync / DeleteAsync —— 还未实现用户的创建/更新/删除逻辑。
  • 注意 / 潜在问题:
    • 使用 Task.Run 包装同步、廉价的属性访问是多余的。应直接返回 Task.FromResult(...) 或直接声明同步返回 Task 的实现。
    • CancellationToken 传递给 Task.Run 并不能真正取消属性访问逻辑,且不必要。
    • 将“规范化用户名”映射到相同的 Name 属性可能不符合 Identity 的设计(通常 Name 和 NormalizedName 应分开存储,规范化用于查找/比较)。
    • SetUserNameAsync 与 SetNormalizedUserNameAsync 都使用 ThrowIfNull,但签名允许 null,可能导致设计不一致。
    • FindByNameAsync 没有使用 cancellationToken,可能是遗漏。
  1. UserInfoClaimsPrincipalFactory : IUserClaimsPrincipalFactory
  • CreateAsync(VmUserInfo user):
    • 通过反射(user.GetType().GetProperties())把 VmUserInfo 的每个属性都转换成 Claim(name=属性名, value=属性值的 ToString()),然后把这些 Claim 放到 ClaimsIdentity 中。
    • 另外显式添加了 ClaimTypes.NameIdentifier,值为 user.Id。
    • 构造 ClaimsIdentity 时,使用:
      • authenticationType = Program.AuthorizeCookieName
      • nameType = "Name"
      • roleType = "Permissions"
  • 行为/问题/注意点:
    • 反射把所有属性都当作 claim:可能把敏感信息(如密码哈希、邮箱验证令牌、个人隐私)也放入 cookie/claims,存在信息泄漏风险。应显式选择需要的属性作为 claim。
    • 将所有属性强制 ToString() 可能导致不期望的字符串格式或丢失信息。
    • ClaimsIdentity 的 nameType 和 roleType 用的是字符串 "Name" 和 "Permissions",通常应使用标准常量 ClaimTypes.Name 和 ClaimTypes.Role(或至少用与系统一致的 claim type),否则框架中某些基于 ClaimTypes.Name/Role 的逻辑可能不起作用。
    • 另外,反射已经会包含 Id 属性(如果存在),还额外添加 NameIdentifier 会重复,但这是可接受的(要注意重复 claim 的含义)。
  1. IApplicationSignInManager / ApplicationSignInManager
  • IApplicationSignInManager 定义了 SignInAsync 和 RefreshSignInAsync 的签名。
  • ApplicationSignInManager 继承 SignInManager 并实现 IApplicationSignInManager:
    • 依赖注入了 userManager、IHttpContextAccessor、IUserClaimsPrincipalFactory、IOptions、ILogger、IAuthenticationSchemeProvider、IUserConfirmation、IFusionCache 等。
    • SignInAsync 覆盖:
      • 先通过 base.CreateUserPrincipalAsync(user) 创建 ClaimsPrincipal(基于 IUserClaimsPrincipalFactory)。
      • 若提供了 authenticationMethod 参数,会在 principal 的第一个 Identity 上添加一个特定名称的 claim(authenticationmethod)。
      • 使用 Context.SignInAsync(Program.AuthorizeCookieName, principal, authenticationProperties) 写入 cookie(或相应的认证方案)。
      • 把 user 对象序列化并写入 fusionCache,key 为 $"account-info-{user.Id}",过期 14 天。
    • RefreshSignInAsync:
      • 先通过 Context.AuthenticateAsync(Program.AuthorizeCookieName) 读取当前 cookie 的 AuthenticationResult。
      • 生成最新的 ClaimsPrincipal(调用 _claimsFactory.CreateAsync(user))。
      • 如果旧的 principal 包含 authenticationmethod claim,而新的 principal 没有,则把旧的 authenticationmethod claim 复制到新 principal(保持原有认证方法信息)。
      • 使用旧的 AuthenticationProperties(若为空则新建)重新 SignInAsync,以替换 cookie 中的 principal(保留原 properties,如过期信息等)。
  • 注意 / 行为:
    • SignInAsync 中将 user 放入缓存,有利于减少后续从数据库查询;但要注意缓存一致性(用户更新时需失效)。
    • RefreshSignInAsync 逻辑会尽量保留旧认证相关的 claim(authentication method),这是为了不丢失外部认证信息。
    • 使用 Program.AuthorizeCookieName 作为 authenticationType,这个选择取决于程序中对该常量的含义。

改进建议(安全性、性能、正确性)

  • UserStore:
    • 把简单同步实现改为 Task.FromResult(...),避免不必要的 Task.Run。
    • 明确处理 NormalizedUserName 与 UserName 的区别,最好在 VmUserInfo 中增加 NormalizedName 字段或在 SetNormalizedUserNameAsync 中执行规范化。
    • 实现 CreateAsync/UpdateAsync/DeleteAsync,或明确抛出适当的异常/返回 IdentityResult.Failed。
    • 统一传递并使用 CancellationToken(尤其是调用到 IAccountService 时)。
  • ClaimsPrincipalFactory:
    • 不要盲目反射所有属性为 claims。显式声明需要的 claim 列表(例如 Id, UserName, Email, Roles 等)。
    • 使用标准 ClaimTypes(如 ClaimTypes.Name、ClaimTypes.Role、ClaimTypes.NameIdentifier)。
    • 小心将敏感字段加入 Claim(cookie 里常驻的 claim 可被客户端读到,或随请求发送)。
  • ApplicationSignInManager:
    • 当在 SignInAsync 写入缓存时,确保在用户更新/登出/更改权限时能正确失效缓存。
    • 检查是否需要将 authenticationMethod 的 claim 用标准 ClaimTypes 或已定义常量,而不是硬编码字符串。
  • 其它小点:
    • 注意重复 claim(例如反射得到 Id 以及显式添加 NameIdentifier)。
    • 代码中部分外部方法(GetUserInfoByNameAsync)未传递 cancellationToken,建议一致化。
    • 增加错误处理与日志,尤其是在外部服务调用(accountService、fusionCache)处。

简短总结

  • 这套代码实现了将 VmUserInfo 和 ASP.NET Identity 连接起来的基本骨架:查询用户(通过 IAccountService)、把用户转成 ClaimsPrincipal、写入认证 cookie 并缓存用户信息、以及刷新 cookie 时替换 principal 的过程。
  • 目前实现有若干不完整(Create/Update/Delete 未实现)和可改进之处(反射生成 claims、Task.Run 用法、规范化用户名处理、敏感数据风险等),建议按上面的改进建议做相应修正。
评论加载中...