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 等外部服务。
逐类说明
- 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,可能是遗漏。
- 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 的含义)。
- 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,如过期信息等)。
- 依赖注入了 userManager、IHttpContextAccessor、IUserClaimsPrincipalFactory、IOptions
- 注意 / 行为:
- 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 用法、规范化用户名处理、敏感数据风险等),建议按上面的改进建议做相应修正。
评论加载中...