@
雙因素認證(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服務商管理端後臺申請一個簡訊模板,用於雙因素認證。
開啟簡訊驗證碼的領域服務類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,
};
}
至此,雙因素認證的後端邏輯已經完成,接下來我們將補充「記住」功能,實現一段時間內免驗證。
本文來自部落格園,作者:林曉lx,轉載請註明原文連結:https://www.cnblogs.com/jevonsflash/p/17297520.html