- 原理
- 用户验证码校验模块
- 双因素认证模块
- 改写登录
在之前的博文 用Abp实现短信验证码免密登录(一):短信校验模块 一文中,我们实现了用户验证码校验模块,今天来拓展这个模块,使Abp用户系统支持双因素认证(Two-Factor Authentication)功能。
国内大多数网站在登录屏正常登录后,检查是否有必要进行二次验证,如果有必要则进入二阶段验证屏,如下图:
本示例基于之前的博文内容,你需要登录并绑定正确的手机号,才能使用双因素认证。示例代码已经放在了GitHub上:Github:matoapp-samples
原理
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.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作为短信服务库。
这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服务商管理端后台申请一个短信模板,用于双因素认证。
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。
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("验证提供者错误";
}
}
改写登录
接下来将双因素认证逻辑添加到登录流程中。
添加类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服务对象
[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)登录(一):认证模块