用Abp实现双因素认证(Two-Factor Authentication, 2FA)登录(一):认证模块

@

在之前的博文 用Abp实现短信验证码免密登录(一):短信校验模块 一文中,我们实现了用户验证码校验模块,今天来拓展这个模块,使Abp用户系统支持双因素认证(Two-Factor Authentication)功能。

双因素认证(Two-Factor Authentication,简称 2FA)是使用两个或多个因素的任意组合来验证用户身份,例如用户提供密码后,还要提供短消息发送的验证码,以证明用户确实拥有该手机。

国内大多数网站在登录屏正常登录后,检查是否有必要进行二次验证,如果有必要则进入二阶段验证屏,如下图:

用Abp实现双因素认证(Two-Factor Authentication, 2FA)登录(一):认证模块

用Abp实现双因素认证(Two-Factor Authentication, 2FA)登录(一):认证模块

接下来就来实践这个小项目

本示例基于之前的博文内容,你需要登录并绑定正确的手机号,才能使用双因素认证。示例代码已经放在了GitHub上:Github:matoapp-samples

原理

查看Abp源码,Abp帮我们定义了几个Setting,用于配置双因素认证的相关功能。确保在数据库中将Abp.Zero.UserManagement.TwoFactorLogin.IsEnabled打开。

public static class TwoFactorLogin {     /// <summary>     /// "Abp.Zero.UserManagement.TwoFactorLogin.IsEnabled".     /// </summary>     public const string IsEnabled = "Abp.Zero.UserManagement.TwoFactorLogin.IsEnabled";      /// <summary>     /// "Abp.Zero.UserManagement.TwoFactorLogin.IsEmailProviderEnabled".     /// </summary>     public const string IsEmailProviderEnabled = "Abp.Zero.UserManagement.TwoFactorLogin.IsEmailProviderEnabled";      /// <summary>     /// "Abp.Zero.UserManagement.TwoFactorLogin.IsSmsProviderEnabled".     /// </summary>     public const string IsSmsProviderEnabled = "Abp.Zero.UserManagement.TwoFactorLogin.IsSmsProviderEnabled";  ... }  

在AbpUserManager的GetValidTwoFactorProvidersAsync方法中

Abp.Zero.UserManagement.TwoFactorLogin.IsSmsProviderEnabled开启后将添加“Phone”到Provider中,将启用短信验证方式。

Abp.Zero.UserManagement.TwoFactorLogin.IsEmailProviderEnabled开启后将添加“Email”到Provider中,将启用邮箱验证方式。

var isEmailProviderEnabled = await IsTrueAsync(     AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsEmailProviderEnabled,     user.TenantId );  if (provider == "Email" && !isEmailProviderEnabled) {     continue; }  var isSmsProviderEnabled = await IsTrueAsync(     AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsSmsProviderEnabled,     user.TenantId );  if (provider == "Phone" && !isSmsProviderEnabled) {     continue; } 

在迁移中添加双因素认证的配置项

//双因素认证 AddSettingIfNotExists(AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsEnabled, "true", tenantId); AddSettingIfNotExists(AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsSmsProviderEnabled, "true", tenantId); AddSettingIfNotExists(AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsEmailProviderEnabled, "true", tenantId); 

将默认User的IsTwoFactorEnabled字段设为true

public User() {     this.IsTwoFactorEnabled= true; } 

用户验证码校验模块

使用AbpBoilerplate.Sms作为短信服务库。

之前定义了DomainService接口,已经实现了验证码的发送、验证码校验、解绑手机号、绑定手机号

这4个功能,通过定义用途(purpose)字段以校验区分短信模板

public interface ICaptchaManager {     Task BindAsync(string token);     Task UnbindAsync(string token);     Task SendCaptchaAsync(long userId, string phoneNumber, string purpose);     Task<bool> VerifyCaptchaAsync(string token, string purpose = "IDENTITY_VERIFICATION"); } 

添加一个用于双因素认证的purpose,在CaptchaPurpose枚举类型中添加TWO_FACTOR_AUTHORIZATION

public const string TWO_FACTOR_AUTHORIZATION = "TWO_FACTOR_AUTHORIZATION";  

在SMS服务商管理端后台申请一个短信模板,用于双因素认证。

用Abp实现双因素认证(Two-Factor Authentication, 2FA)登录(一):认证模块

打开短信验证码的领域服务类SmsCaptchaManager, 添加TWO_FACTOR_AUTHORIZATION对应短信模板的编号

public async Task SendCaptchaAsync(long userId, string phoneNumber, string purpose) {     var captcha = CommonHelper.GetRandomCaptchaNumber();     var model = new SendSmsRequest();     model.PhoneNumbers = new string[] { phoneNumber };     model.SignName = "MatoApp";     model.TemplateCode = purpose switch     {         CaptchaPurpose.BIND_PHONENUMBER => "SMS_255330989",         CaptchaPurpose.UNBIND_PHONENUMBER => "SMS_255330923",         CaptchaPurpose.LOGIN => "SMS_255330901",         CaptchaPurpose.IDENTITY_VERIFICATION => "SMS_255330974"         CaptchaPurpose.TWO_FACTOR_AUTHORIZATION => "SMS_1587660"    //添加双因素认证对应短信模板的编号     };      ... } 

双因素认证模块

创建双因素认证领域服务类TwoFactorAuthorizationManager。

创建方法IsTwoFactorAuthRequiredAsync,返回登录用户是否需要双因素认证,若未开启TwoFactorLogin.IsEnabled、用户未开启双因素认证,或没有添加验证提供者,则跳过双因素认证。

public async Task<bool> IsTwoFactorAuthRequiredAsync(AbpLoginResult<Tenant, User> loginResult) {     if (!await settingManager.GetSettingValueAsync<bool>(AbpZeroSettingNames.UserManagement.TwoFactorLogin.IsEnabled))     {         return false;     }      if (!loginResult.User.IsTwoFactorEnabled)     {         return false;     }     if ((await _userManager.GetValidTwoFactorProvidersAsync(loginResult.User)).Count <= 0)     {         return false;     }     return true; }  

创建TwoFactorAuthenticateAsync,此方法根据回传的provider和token值校验用户是否通过双因素认证。

public async Task TwoFactorAuthenticateAsync(User user, string token, string provider) {     if (provider == "Email")     {         var isValidate = await emailCaptchaManager.VerifyCaptchaAsync(token, CaptchaPurpose.TWO_FACTOR_AUTHORIZATION);         if (!isValidate)         {             throw new UserFriendlyException("验证码错误");         }     }      else if (provider == "Phone")     {         var isValidate = await smsCaptchaManager.VerifyCaptchaAsync(token, CaptchaPurpose.TWO_FACTOR_AUTHORIZATION);         if (!isValidate)         {             throw new UserFriendlyException("验证码错误");         }     }     else     {         throw new UserFriendlyException("验证码提供者错误");     }       }  

创建SendCaptchaAsync,此方用于发送验证码。

public async Task SendCaptchaAsync(long userId, string Provider) {     var user = await _userManager.FindByIdAsync(userId.ToString());     if (user == null)     {         throw new UserFriendlyException("找不到用户");      }      if (Provider == "Email")     {         if (!user.IsEmailConfirmed)         {             throw new UserFriendlyException("未绑定邮箱");         }         await emailCaptchaManager.SendCaptchaAsync(user.Id, user.EmailAddress, CaptchaPurpose.TWO_FACTOR_AUTHORIZATION);     }     else if (Provider == "Phone")     {         if (!user.IsPhoneNumberConfirmed)         {             throw new UserFriendlyException("未绑定手机号");         }         await smsCaptchaManager.SendCaptchaAsync(user.Id, user.PhoneNumber, CaptchaPurpose.TWO_FACTOR_AUTHORIZATION);     }     else     {         throw new UserFriendlyException("验证提供者错误");     } } 

改写登录

接下来将双因素认证逻辑添加到登录流程中。

在web.core项目中,
添加类SendTwoFactorAuthenticateCaptchaModel,发送验证码时将一阶段返回的userId和选择验证方式的provider传入

public class SendTwoFactorAuthenticateCaptchaModel {     [Range(1, long.MaxValue)]     public long UserId { get; set; }      [Required]     public string Provider { get; set; } } 

将验证码Token,和验证码提供者Provider的定义添加到AuthenticateModel中

public string TwoFactorAuthenticationToken { get; set; }  public string TwoFactorAuthenticationProvider { get; set; } 

将提供者列表TwoFactorAuthenticationProviders,和是否需要双因素认证RequiresTwoFactorAuthenticate的定义添加到AuthenticateResultModel中

public bool RequiresTwoFactorAuthenticate { get; set; }  public IList<string> TwoFactorAuthenticationProviders { get; set; } 

打开TokenAuthController,注入UserManager和TwoFactorAuthorizationManager服务对象

添加终节点SendTwoFactorAuthenticateCaptcha,用于前端调用发送验证码

[HttpPost] public async Task SendTwoFactorAuthenticateCaptcha([FromBody] SendTwoFactorAuthenticateCaptchaModel model) {     await twoFactorAuthorizationManager.SendCaptchaAsync(model.UserId, model.Provider); } 

改写Authenticate方法如下:

[HttpPost] public async Task<AuthenticateResultModel> Authenticate([FromBody] AuthenticateModel model) {     //用户名密码校验     var loginResult = await GetLoginResultAsync(         model.UserNameOrEmailAddress,         model.Password,         GetTenancyNameOrNull()     );      await userManager.InitializeOptionsAsync(loginResult.Tenant?.Id);      //判断是否需要双因素认证     if (await twoFactorAuthorizationManager.IsTwoFactorAuthRequiredAsync(loginResult))     {         //判断是否一阶段         if (string.IsNullOrEmpty(model.TwoFactorAuthenticationToken))         {             //一阶登录完成,返回结果,等待二阶段登录             return new AuthenticateResultModel             {                 RequiresTwoFactorAuthenticate = true,                 UserId = loginResult.User.Id,                 TwoFactorAuthenticationProviders = await userManager.GetValidTwoFactorProvidersAsync(loginResult.User),              };         }         //二阶段,双因素认证校验         else         {             await twoFactorAuthorizationManager.TwoFactorAuthenticateAsync(loginResult.User, model.TwoFactorAuthenticationToken, model.TwoFactorAuthenticationProvider);         }     }      //二阶段完成,返回最终登录结果     var accessToken = CreateAccessToken(CreateJwtClaims(loginResult.Identity));     return new AuthenticateResultModel     {         AccessToken = accessToken,         EncryptedAccessToken = GetEncryptedAccessToken(accessToken),         ExpireInSeconds = (int)_configuration.Expiration.TotalSeconds,         UserId = loginResult.User.Id,     }; } 

用Abp实现双因素认证(Two-Factor Authentication, 2FA)登录(一):认证模块

至此,双因素认证的后端逻辑已经完成,接下来我们将补充“记住”功能,实现一段时间内免验证。

发表评论

评论已关闭。

相关文章

当前内容话题