扩展.Net Core Identity Server 授权方式,实现 手机号+ 验证码 登录

背景

国内来讲,注册/登录流程都是尽可能的简单,注册流程复杂,容易流失客户。手机号 + 短信验证码的方式非常普遍;但是框架默认并没有类似的功能,需要我们自己进行扩展。

 

思路

  1. 验证登录手机号为注册用户,且验证码正确;验证通过后,去 Identity Server 获取Token,然后返回客户端。
  2. 扩展 Identity 的授权方式,类似于 Authorization code;关于gtant type 可以参考 Grant Types — IdentityServer4 1.0.0 documentation (identityserver4test.readthedocs.io)

不过扩由于Identity Server 要收费,以及Abp 6.0 要集成 OpenIdDict;展 Grant Type 的方式,可以适用于当前,后续根据需要进行调整。

 

定义 GrantTypes

1  public class IdentityGrantTypes
2     {
3         public const string PhoneCode = "phone_code";
4     }
5         

View Code

 

实现 IExtensionGrantValidator

主要实现对手机号以及短信验证码的校验

 

  1  public class PhoneCodeGrantValidator : IExtensionGrantValidator, ITransientDependency
  2     {
  3         private readonly IOptions<IdentityOptions> _identityOptions;
  4         private readonly IAccountRepository _accountRepository;
  5         private readonly IdentityUserManager _identityUserManager;
  6         private readonly AccountTokenManager _accountTokenManager;
  7 
  8         public string GrantType => IdentityGrantTypes.PhoneCode;
  9 
 10         public PhoneCodeGrantValidator(
 11             IOptions<IdentityOptions> identityOptions,
 12             IAccountRepository accountRepository,
 13             IdentityUserManager identityUserManager,
 14             AccountTokenManager accountTokenManager)
 15         {
 16             _identityOptions = identityOptions;
 17             _accountRepository = accountRepository;
 18             _identityUserManager = identityUserManager;
 19             _accountTokenManager = accountTokenManager;
 20         }
 21 
 22         public async Task ValidateAsync(ExtensionGrantValidationContext context)
 23         {
 24             await _identityOptions.SetAsync();
 25 
 26             var phoneNumber = context.Request.Raw.Get("phoneNumber");
 27             var code = context.Request.Raw.Get("code");
 28 
 29             var validateParamsResult = ValidateRequestParams(phoneNumber, code);
 30             if (!validateParamsResult.IsNullOrWhiteSpace())
 31             {
 32                 SetContextError(validateParamsResult, context);
 33                 return;
 34             }
 35             
 36             var identityUser = await _accountRepository.FindByConfirmedPhoneAsync(phoneNumber);
 37             if (identityUser == null)
 38             {
 39                 SetContextError("无效的手机号", context);
 40                 return;
 41             }
 42             
 43             if (await _identityUserManager.IsLockedOutAsync(identityUser))
 44             {
 45                 SetContextError("账户已锁定", context);
 46                 return;
 47             }
 48 
 49             var validateCodeResult = await ValidateCodeLoginAsync(phoneNumber, code);
 50             if (!validateCodeResult.IsNullOrWhiteSpace())
 51             {
 52                 await _identityUserManager.AccessFailedAsync(identityUser);
 53                 SetContextError(validateCodeResult, context);
 54                 return;
 55             }
 56 
 57             var claims = new List<Claim>
 58             {
 59                 new("phoneNumber", phoneNumber)
 60             };
 61 
 62             if (identityUser.TenantId.HasValue)
 63             {
 64                 claims.Add(new Claim(AbpClaimTypes.TenantId, identityUser.TenantId?.ToString()));
 65             }
 66 
 67             claims.AddRange(identityUser.Claims.Select(
 68                 item => new Claim(item.ClaimType, item.ClaimValue)));
 69 
 70             context.Result = new GrantValidationResult(identityUser.Id.ToString(), GrantType, claims);
 71         }
 72 
 73         public async Task<string> ValidateCodeLoginAsync(string phoneNumber, string code)
 74         {
 75             var isValidCode = await _accountTokenManager.VerifySignInCodeAsync(phoneNumber, code);
 76 
 77             return !isValidCode ? "无效的手机号或验证码" : string.Empty;
 78         }
 79 
 80         private static string ValidateRequestParams(
 81             string phoneNumber, string code)
 82         {
 83             if (string.IsNullOrWhiteSpace(phoneNumber))
 84             {
 85                 return "手机号不能为空";
 86             }
 87 
 88             if (string.IsNullOrWhiteSpace(code))
 89             {
 90                 return "验证码不能为空";
 91             }
 92 
 93             return string.Empty;
 94         }
 95 
 96         private static void SetContextError(
 97             string errorMessage, ExtensionGrantValidationContext context)
 98         {
 99             context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant)
100             {
101                 ErrorDescription = errorMessage
102             };
103         }
104     }

View Code

 

注册扩展服务

1   public override void PreConfigureServices(ServiceConfigurationContext context)
2     {
3         PreConfigure<IIdentityServerBuilder>(builder =>
4         {
5             builder.AddExtensionGrantValidator<PhoneCodeGrantValidator>();
6         });
7     }

 

简单验证

至此,扩展方式的核心工作已经准备完成,可以通过 postman 进行简单的实验。

 

 

非扩展授权方式

此方式也比较简单,校验手机号以及验证码的主体逻辑一致,只需要验证用户之后,通过 httpClient 去 IdentityServer 获取token,然后返回客户端即可。

其他:为了更好的安全,在登录失败后需要显式的标记登录失败,配合 Identity 的一些策略,可以对一段时间内登录失败次数过多的账户,进行锁定,防止用户信息泄露。

 

 

结尾

近期已经从公司离职了。近期也思考了很多,大城市与二线城市在做事风格上确实差别比较大;也有很多令人唏嘘的事情,改天总结一下。