擴展.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 的一些策略,可以對一段時間內登錄失敗次數過多的賬戶,進行鎖定,防止用戶資訊泄露。

 

 

結尾

近期已經從公司離職了。近期也思考了很多,大城市與二線城市在做事風格上確實差別比較大;也有很多令人唏噓的事情,改天總結一下。