深入理解Aspnet Core之Identity(3)

主题

账户管理一个比较常见的功能就是密码强度策略,Identity已经内置了一个通用的可配置的策略,我们一般情况下可以直接拿来用即可。本篇我会介绍一些
Identity内置的密码策略类:PasswordValidator,并且简单介绍一下源码。最好我们还会自定义一个密码策略类的实现。


密码强度配置

我们需要在startup类里面配置,密码强度策略。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppIdentityDbContext>(options =>
options.UseSqlServer(
Configuration["Data:AppStoreIdentity:ConnectionString"]));
//配置密码强度
services.AddIdentity<AppUser, IdentityRole>(opts => {
opts.Password.RequiredLength = 6;
opts.Password.RequireNonAlphanumeric = false;
opts.Password.RequireLowercase = false;
opts.Password.RequireUppercase = false;
opts.Password.RequireDigit = false;
}).AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
}

这个 AddIdentity 方法接受一个 IdentityOptions 对象参数,这个对象包含了所有能用于配置Identity系统的配置项,其中就包含了密码强度的配置。PassWord属性包含了可用于密码强度的配置项,其中包括:

  • RequiredLength :这个属性用于指明密码的最小长度,类型 int
  • RequireNonAlphanumeric :类型:bool,true代表密码至少包含一个非字母或者数字的字符,比如 !@#
  • RequireLowercase :类型bool,true代表密码至少包含一个小写字母
  • RequireUppercase :类型 bool,true 代表密码至少包含一个大写字母
  • RequireDigit :类型 bool,true 代表密码至少包含一个数字
    这里我配置的密码强度是不少于六位的字符即可。

    IdentityOptions 类可用于配置的属性

    在这里我们把IdentityOptions对应的源代码展现给大家,让大家了解一下哪些可用于配置Identity,
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    /// <summary>
    /// Represents all the options you can use to configure the identity system.
    /// </summary>
    public class IdentityOptions
    {
    /// <summary>
    /// Gets or sets the <see cref="ClaimsIdentityOptions"/> for the identity system.
    /// </summary>
    /// <value>
    /// The <see cref="ClaimsIdentityOptions"/> for the identity system.
    /// </value>
    public ClaimsIdentityOptions ClaimsIdentity { get; set; } = new ClaimsIdentityOptions();

    /// <summary>
    /// Gets or sets the <see cref="UserOptions"/> for the identity system.
    /// </summary>
    /// <value>
    /// The <see cref="UserOptions"/> for the identity system.
    /// </value>
    public UserOptions User { get; set; } = new UserOptions();

    /// <summary>
    /// Gets or sets the <see cref="PasswordOptions"/> for the identity system.
    /// </summary>
    /// <value>
    /// The <see cref="PasswordOptions"/> for the identity system.
    /// </value>
    public PasswordOptions Password { get; set; } = new PasswordOptions();

    /// <summary>
    /// Gets or sets the <see cref="LockoutOptions"/> for the identity system.
    /// </summary>
    /// <value>
    /// The <see cref="LockoutOptions"/> for the identity system.
    /// </value>
    public LockoutOptions Lockout { get; set; } = new LockoutOptions();

    /// <summary>
    /// Gets or sets the <see cref="SignInOptions"/> for the identity system.
    /// </summary>
    /// <value>
    /// The <see cref="SignInOptions"/> for the identity system.
    /// </value>
    public SignInOptions SignIn { get; set; } = new SignInOptions();

    /// <summary>
    /// Gets or sets the <see cref="TokenOptions"/> for the identity system.
    /// </summary>
    /// <value>
    /// The <see cref="TokenOptions"/> for the identity system.
    /// </value>
    public TokenOptions Tokens { get; set; } = new TokenOptions();

    /// <summary>
    /// Gets or sets the <see cref="StoreOptions"/> for the identity system.
    /// </summary>
    /// <value>
    /// The <see cref="StoreOptions"/> for the identity system.
    /// </value>
    public StoreOptions Stores { get; set; } = new StoreOptions();
    }

我们除了可以用 IdentityOptions 配置密码策略,还可以用于配置 持久化配置StoreOptions,令牌配置TokenOption等;


自定义实现IPasswordValidator

  • 我们首先看一下 IPasswordValidator 接口的定义;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Identity
{
/// <summary>
/// Provides an abstraction for validating passwords.
/// </summary>
/// <typeparam name="TUser">The type that represents a user.</typeparam>
public interface IPasswordValidator<TUser> where TUser : class
{
/// <summary>
/// Validates a password as an asynchronous operation.
/// </summary>
/// <param name="manager">The <see cref="UserManager{TUser}"/> to retrieve the <paramref name="user"/> properties from.</param>
/// <param name="user">The user whose password should be validated.</param>
/// <param name="password">The password supplied for validation</param>
/// <returns>The task object representing the asynchronous operation.</returns>
Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user, string password);
}
}

这个接口接受一个泛型T,这个T是应用指定的用户类User,这里我们是 AppUser类。这个接口包含类一个方法 ValidateAsync,这是一个异步方法,用于验证密码是否符合策略。这个方法返回 IdentityResult 类,假如验证没有问题,你可以用静态的IdentityResult.Success属性返回该对象,反之,用Failed静态属性返回。

  • 现在我们去了解一下IPasswordValidator接口的默认实现,先附上源代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    // Copyright (c) .NET Foundation. All rights reserved.
    // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;

    namespace Microsoft.AspNetCore.Identity
    {
    /// <summary>
    /// Provides the default password policy for Identity.
    /// </summary>
    /// <typeparam name="TUser">The type that represents a user.</typeparam>
    public class PasswordValidator<TUser> : IPasswordValidator<TUser> where TUser : class
    {
    /// <summary>
    /// Constructions a new instance of <see cref="PasswordValidator{TUser}"/>.
    /// </summary>
    /// <param name="errors">The <see cref="IdentityErrorDescriber"/> to retrieve error text from.</param>
    public PasswordValidator(IdentityErrorDescriber errors = null)
    {
    Describer = errors ?? new IdentityErrorDescriber();
    }

    /// <summary>
    /// Gets the <see cref="IdentityErrorDescriber"/> used to supply error text.
    /// </summary>
    /// <value>The <see cref="IdentityErrorDescriber"/> used to supply error text.</value>
    public IdentityErrorDescriber Describer { get; private set; }

    /// <summary>
    /// Validates a password as an asynchronous operation.
    /// </summary>
    /// <param name="manager">The <see cref="UserManager{TUser}"/> to retrieve the <paramref name="user"/> properties from.</param>
    /// <param name="user">The user whose password should be validated.</param>
    /// <param name="password">The password supplied for validation</param>
    /// <returns>The task object representing the asynchronous operation.</returns>
    public virtual Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user, string password)
    {
    if (password == null)
    {
    throw new ArgumentNullException(nameof(password));
    }
    if (manager == null)
    {
    throw new ArgumentNullException(nameof(manager));
    }
    var errors = new List<IdentityError>();
    var options = manager.Options.Password;
    if (string.IsNullOrWhiteSpace(password) || password.Length < options.RequiredLength)
    {
    errors.Add(Describer.PasswordTooShort(options.RequiredLength));
    }
    if (options.RequireNonAlphanumeric && password.All(IsLetterOrDigit))
    {
    errors.Add(Describer.PasswordRequiresNonAlphanumeric());
    }
    if (options.RequireDigit && !password.Any(IsDigit))
    {
    errors.Add(Describer.PasswordRequiresDigit());
    }
    if (options.RequireLowercase && !password.Any(IsLower))
    {
    errors.Add(Describer.PasswordRequiresLower());
    }
    if (options.RequireUppercase && !password.Any(IsUpper))
    {
    errors.Add(Describer.PasswordRequiresUpper());
    }
    if (options.RequiredUniqueChars >= 1 && password.Distinct().Count() < options.RequiredUniqueChars)
    {
    errors.Add(Describer.PasswordRequiresUniqueChars(options.RequiredUniqueChars));
    }
    return
    Task.FromResult(errors.Count == 0
    ? IdentityResult.Success
    : IdentityResult.Failed(errors.ToArray()));
    }

    /// <summary>
    /// Returns a flag indicating whether the supplied character is a digit.
    /// </summary>
    /// <param name="c">The character to check if it is a digit.</param>
    /// <returns>True if the character is a digit, otherwise false.</returns>
    public virtual bool IsDigit(char c)
    {
    return c >= '0' && c <= '9';
    }

    /// <summary>
    /// Returns a flag indicating whether the supplied character is a lower case ASCII letter.
    /// </summary>
    /// <param name="c">The character to check if it is a lower case ASCII letter.</param>
    /// <returns>True if the character is a lower case ASCII letter, otherwise false.</returns>
    public virtual bool IsLower(char c)
    {
    return c >= 'a' && c <= 'z';
    }

    /// <summary>
    /// Returns a flag indicating whether the supplied character is an upper case ASCII letter.
    /// </summary>
    /// <param name="c">The character to check if it is an upper case ASCII letter.</param>
    /// <returns>True if the character is an upper case ASCII letter, otherwise false.</returns>
    public virtual bool IsUpper(char c)
    {
    return c >= 'A' && c <= 'Z';
    }

    /// <summary>
    /// Returns a flag indicating whether the supplied character is an ASCII letter or digit.
    /// </summary>
    /// <param name="c">The character to check if it is an ASCII letter or digit.</param>
    /// <returns>True if the character is an ASCII letter or digit, otherwise false.</returns>
    public virtual bool IsLetterOrDigit(char c)
    {
    return IsUpper(c) || IsLower(c) || IsDigit(c);
    }
    }
    }

默认的实现类,除了实现了ValidateAsync方法,还有IsDigit IsLower等方法,他们对应了密码配置的属性,比如是否小写,是否大写等等…密码验证方法里面主要根据应用设置的密码强度属性来进行一次判断,代码比较好理解。


自定义实现密码强度验证

虽然内置的密码验证已经满足了大部分应用场景,但有的时候我们还是需要自定义实现一个满足自身业务需求的密码验证类。这里我给大家演示一下怎么去自定义个密码验证类去满足令人头疼业务需求。假设我们有这样一个业务需求:

  • 要求密码不能包含敏感字符:china
    1.首先在项目中创建一个Infrastructure 文件夹,在该文件夹添加一个CustomPasswordValidator类,代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    namespace DemoUser.Infrastructure
    {
    public class CustomPasswordValidator : PasswordValidator<AppUser>
    {
    public override async Task<IdentityResult> ValidateAsync(
    UserManager<AppUser> manager, AppUser user, string password)
    {
    IdentityResult result = await base.ValidateAsync(manager,
    user, password);
    List<IdentityError> errors = result.Succeeded ? new List<IdentityError>() : result.Errors.ToList();

    if (password.Contains("china"))
    {
    errors.Add(new IdentityError
    {
    Code = "PasswordContainsillegalletter",
    Description = "Password cannot contain china"
    });
    }

    return errors.Count == 0
    ? IdentityResult.Success
    : IdentityResult.Failed(errors.ToArray());
    }
    }
    }

2.再次我们需要在startup类里面把我们自定义的密码验证类依赖注入到应用中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void ConfigureServices(IServiceCollection services)
{
//注入自定义密码验证类
services.AddTransient<IPasswordValidator<AppUser>,
CustomPasswordValidator>();

services.AddDbContext<AppIdentityDbContext>(options =>
options.UseSqlServer(
Configuration["Data:AppStoreIdentity:ConnectionString"]));
//配置密码强度
services.AddIdentity<AppUser, IdentityRole>(opts => {
opts.Password.RequiredLength = 6;
opts.Password.RequireNonAlphanumeric = false;
opts.Password.RequireLowercase = false;
opts.Password.RequireUppercase = false;
opts.Password.RequireDigit = false;
}).AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
}
  • 这样我们的自定义的密码验证类就成功替换掉了内置的密码验证类。

后记

简单介绍了IPasswordValidator的配置和简单自定义,下一篇我打算讲解一下Identity架构的设计,以及我们可以自定义的地方有哪些。
代码对应的地址 github 对应分支f3

文章目录
  1. 1. 主题
  2. 2. 密码强度配置
  3. 3. IdentityOptions 类可用于配置的属性
  4. 4. 自定义实现IPasswordValidator
  5. 5. 自定义实现密码强度验证
  6. 6. 后记
|