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


    @


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

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

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

    在这里插入图片描述

    在这里插入图片描述

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

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

    原理

    双因素认证可以拆成两个阶段,第一阶段是普通的用户名+密码登录,一阶段验证是整个身份验证的基础,确保认证安全。整个认证流程如下

    在这里插入图片描述

    是否有必要开启双因素认证是由系统是否开启两步验证、用户是否启用两步验证以及是否通过免登录验证决定的,其中免登录验证将在后续文章中介绍。

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

    public static class TwoFactorLogin
    {
        /// 
        /// "Abp.Zero.UserManagement.TwoFactorLogin.IsEnabled".
        /// 
        public const string IsEnabled = "Abp.Zero.UserManagement.TwoFactorLogin.IsEnabled";
    
        /// 
        /// "Abp.Zero.UserManagement.TwoFactorLogin.IsEmailProviderEnabled".
        /// 
        public const string IsEmailProviderEnabled = "Abp.Zero.UserManagement.TwoFactorLogin.IsEmailProviderEnabled";
    
        /// 
        /// "Abp.Zero.UserManagement.TwoFactorLogin.IsSmsProviderEnabled".
        /// 
        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服务商管理端后台申请一个短信模板,用于双因素认证。

    在这里插入图片描述

    打开短信验证码的领域服务类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 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 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,
        };
    }
    

    在这里插入图片描述

    至此,双因素认证的后端逻辑已经完成。

    项目地址

    Github:matoapp-samples

  • 相关阅读:
    AJAX的使用,搭建web服务器,AJAX响应消息类型,JSON
    2023美亚杯个人赛复盘(三)
    【JavaScript】Date对象(创建时间对象、常用Date方法总结)
    前后端传参
    JAVA线程池 -clt设计与分析
    opencv c++ 图像梯度、边缘、锐化
    5月13号作业
    gitlab创建一个项目的流程
    猿创征文 | 专做药品生产研发的程序员
    python计算阶层
  • 原文地址:https://www.cnblogs.com/jevonsflash/p/17297520.html