查看: 112|回复: 0

ASP.NET Core Identity 源码解析系列 —— 设置用户密码

[复制链接]

4

主题

7

帖子

14

积分

新手上路

Rank: 1

积分
14
发表于 2023-4-5 08:18:35 | 显示全部楼层 |阅读模式
环境


  • http://ASP.NET Core 3.1
  • EntityFramework Core 3.0
结论

使用UserManager来设置用户密码通过了以下两个步骤:

  • 使用PasswordHasher来生成密码Hash;
  • 将密码Hash赋值给User的PasswordHash属性;
虽然过程只有两步,但是整个源码还是有点复杂的。
// Todo : 在这里加入一个整体的调用过程图
源码分析过程

在给用户添加密码时,通常会调用UserManager<TUser>.AddPasswordAsync(TUser, String)方法。源码如下(源码地址UserManager.cs),
public virtual async Task<IdentityResult> AddPasswordAsync(TUser user, string password)
{
    ThrowIfDisposed();
    var passwordStore = GetPasswordStore();
    if (user == null) {
        throw new ArgumentNullException(nameof(user));
    }
    var hash = await passwordStore.GetPasswordHashAsync(user, CancellationToken);
    if (hash != null) {
        Logger.LogWarning(1, "User {userId} already has a password.", await GetUserIdAsync(user));
        return IdentityResult.Failed(ErrorDescriber.UserAlreadyHasPassword());
    }
    var result = await UpdatePasswordHash(passwordStore, user, password);
    if (!result.Succeeded) {
        return result;
    }
    return await UpdateUserAsync(user);
}
这段代码首先调用了passwordStore.GetPasswordHashAsync方法,来检查用户是否已经设置了密码。如果用户已经设置了密码,则抛出一个异常。如果用户没有设置密码,则调用UpdatePasswordHash方法来为用户设置密码。源码如下(源码地址UserManager.cs):
private async Task<IdentityResult> UpdatePasswordHash(
    IUserPasswordStore<TUser> passwordStore,
    TUser user,
    string newPassword,
    bool validatePassword = true)
{
    if (validatePassword) {
        var validate = await ValidatePasswordAsync(user, newPassword);
        if (!validate.Succeeded) { return validate; }
    }
    var hash = newPassword != null ? PasswordHasher.HashPassword(user, newPassword) : null;
    await passwordStore.SetPasswordHashAsync(user, hash, CancellationToken);
    await UpdateSecurityStampInternal(user);
    return IdentityResult.Success;
}
UpdatePasswordHash方法通过PasswordHasher.HashPassword方法来生成密码Hash,然后调用passwordStore.SetPasswordHashAsync方法来更新用户密码。这两个方法分别存在于PasswordHasher对象和passwordStore对象。PasswordHasher对象比较简单,这里先看一下passwordStore是怎么来的。UpdatePasswordHash方法中的passwordStore是从AddPasswordAsync传递过来的,而AddPasswordAsync中的passwordStore则是通过调用GetPasswordStore方法获得。再看一下GetPasswordStore方法(源码地址UserManager.cs):
private IUserPasswordStore<TUser> GetPasswordStore() {
    var cast = Store as IUserPasswordStore<TUser>;
    if (cast == null) {
        throw new NotSupportedException(Resources.StoreNotIUserPasswordStore);
    }
    return cast;
}
原来passwordStore是由Store对象通过类型转换转换为IUserPasswordStore类型的。Store对象和刚才的PasswordHasher对象都是通过构造函数传递进来的。在http://ASP.NET Core中这些对象都是通过依赖注入加入到IOC容器中的。UserManager构造函数的部分源码如下(源码地址UserManager.cs):
protected internal IUserStore<TUser> Store { get; set; }
public IPasswordHasher<TUser> PasswordHasher { get; set; }

public UserManager(IUserStore<TUser> store,
    IPasswordHasher<TUser> passwordHasher,
    ...)
{
    if (store == null) {
        throw new ArgumentNullException(nameof(store));
    }
    Store = store;
    PasswordHasher = passwordHasher;
    ...
}
值得注意的是,Store对象的类型是IUserStore。然而在设置密码时,却转换成类型IUserPasswordStore。因此,需要查看一下IUserPasswordStore接口定义(源码地址IUserPasswordStore.cs):
public interface IUserPasswordStore<TUser> :
    IUserStore<TUser>
    where TUser : class
{
    Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken);
    Task<string> GetPasswordHashAsync(TUser user, CancellationToken cancellationToken);
    Task<bool> HasPasswordAsync(TUser user, CancellationToken cancellationToken);
}
可以看出来IUserPasswordStore继承于IUserStore,并且定义了三个方法,其中就包括前述GetPasswordHashAsync方法和SetPasswordHashAsync方法。由于是父接口转换成子接口,因此之前在GetPasswordStore方法中进行类型转换后需要判断是否转换成功。
到目前为止,我们已经弄清楚UserManager中设置密码的逻辑。但是IUserStore对象是如何创建并注册进IOC容器的,实现IUserStore接口的具体类型是什么,SetPasswordHashAsync方法具体又进行了什么操作,依然还是不清楚。在http://ASP.NET Core中增加Identity是,使用了如下代码(这里不是http://ASP.NET Core的源码):
services
    .AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();
通过这段代码,猜测应该是在AddEntityFrameworkStores中进行了创建和注册IUserStore,进入源码查看(源码地址IdentityEntityFrameworkBuilderExtensions.cs):
public static IdentityBuilder AddEntityFrameworkStores<TContext>(this IdentityBuilder builder)
    where TContext : DbContext
{
    AddStores(builder.Services, builder.UserType, builder.RoleType, typeof(TContext));
    return builder;
}
这段代码调用AddStores,并且传入了四个参数: 1. builder.Services:IOC容器 2. builder.UserType:用户定义的User类型,此例中为ApplicationUser 3. builder.RoleType:用户定义的Role类型,此例中为IdentityRole 4. typeof(TContext):用户定义的EFCore DbContext类型,此例中为ApplicationDbContext AddStores方法源码如下(源码地址IdentityEntityFrameworkBuilderExtensions.cs)。AddStores函数会根据是否传入roleType来进行不同的创建操作。由于本例中传入了roleType,为了清晰只查看这部分代码:
private static void AddStores(
    IServiceCollection services,
    Type userType,
    Type roleType,
    Type contextType)
{
    // 调用FindGenericBaseType方法获取userType的基类型
    var identityUserType = FindGenericBaseType(userType, typeof(IdentityUser<>));
    if (identityUserType == null) {
        throw new InvalidOperationException(Resources.NotIdentityUser);
    }
    // 获取IdentityUser<>的第一个范型参数,默认是string
    // IdentityUser的定义如下:
    //     public class IdentityUser : IdentityUser<string>
    var keyType = identityUserType.GenericTypeArguments[0];

    ...

    // 调用FindGenericBaseType方法获取roleType的基类型
    var identityRoleType = FindGenericBaseType(roleType, typeof(IdentityRole<>));
    if (identityRoleType == null) {
        throw new InvalidOperationException(Resources.NotIdentityRole);
    }

    Type userStoreType = null;
    Type roleStoreType = null;
    // 调用FindGenericBaseType方法获取contextType的基类型
    var identityContext = FindGenericBaseType(contextType, typeof(IdentityDbContext<,,,,,,,>));
    if (identityContext == null) {
        // identityContext为空,代表contextType不是从IdentityDbContext继承来的
        userStoreType = typeof(UserStore<,,,>).MakeGenericType(userType, roleType, contextType, keyType);
        roleStoreType = typeof(RoleStore<,,>).MakeGenericType(roleType, contextType, keyType);
    }
    else {
        userStoreType = typeof(UserStore<,,,,,,,,>).MakeGenericType(userType, roleType, contextType,
            identityContext.GenericTypeArguments[2],
            identityContext.GenericTypeArguments[3],
            identityContext.GenericTypeArguments[4],
            identityContext.GenericTypeArguments[5],
            identityContext.GenericTypeArguments[7],
            identityContext.GenericTypeArguments[6]);
        roleStoreType = typeof(RoleStore<,,,,>).MakeGenericType(roleType, contextType,
            identityContext.GenericTypeArguments[2],
            identityContext.GenericTypeArguments[4],
            identityContext.GenericTypeArguments[6]);
    }
    // 通过IOC容器创建IUserStore
    services.TryAddScoped(typeof(IUserStore<>).MakeGenericType(userType), userStoreType);
    services.TryAddScoped(typeof(IRoleStore<>).MakeGenericType(roleType), roleStoreType);

    ...
}
在源码中我添加了一些注释来帮助理解。从这里可以发现,AddStores方法根据用户定义的类型,通过IOC容器创建了UserStore<,,,,,,,,>范型类的对象,并以IUserStore类型注册进IOC容器。也就是说,UserManager中传入的Store就是这个UserStore<,,,,,,,,>范型类的对象。为了解决刚才提出的最后一个问题,SetPasswordHashAsync方法具体进行了什么操作,继续查看UserStore的源码(源码地址UserStore.cs):
public class UserStore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> :
        UserStoreBase<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>,
        IProtectedUserStore<TUser>
        where TUser : IdentityUser<TKey>
        where TRole : IdentityRole<TKey>
        where TContext : DbContext
        where TKey : IEquatable<TKey>
        where TUserClaim : IdentityUserClaim<TKey>, new()
        where TUserRole : IdentityUserRole<TKey>, new()
        where TUserLogin : IdentityUserLogin<TKey>, new()
        where TUserToken : IdentityUserToken<TKey>, new()
        where TRoleClaim : IdentityRoleClaim<TKey>, new()
UserStore<,,,,,,,,>继承于UserStoreBase<,,,,,,,,>,并且在UserStore<,,,,,,,,>中并没有SetPasswordHashAsync方法。看来是调用了UserStoreBase<,,,,,,,,>的SetPasswordHashAsync方法。查看源码得知,8个范型参数的UserStoreBase<,,,,,,,,>中也没有SetPasswordHashAsync方法,并且它继承于5个范型参数的UserStoreBase<,,,,>类,于是又继续穿透。UserStoreBase<,,,,>类源码如下(源码地址UserStoreBase.cs):
public virtual Task SetPasswordHashAsync(
    TUser user,
    string passwordHash,
    CancellationToken cancellationToken = default(CancellationToken))
{
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) {
        throw new ArgumentNullException(nameof(user));
    }
    user.PasswordHash = passwordHash;
    return Task.CompletedTask;
}
看到这里终于明白了,原来设置密码就是把计算好的密码Hash赋值给user的属性PasswordHash。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表