Abp.Zero 手機號免密登錄驗證與號碼綁定功能的實現(一):驗證碼模塊
這是一篇系列博文,我將使用Abp.Zero搭建一套集成手機號免密登錄驗證與號碼綁定功能的用戶系統:
- Abp.Zero 手機號免密登錄驗證與號碼綁定功能的實現(一):驗證碼模塊
- Abp.Zero 手機號免密登錄驗證與號碼綁定功能的實現(二):改造Abp默認實現
- Abp.Zero 手機號免密登錄驗證與號碼綁定功能的實現(三):網頁端開發
第三方身份驗證在Abp中稱之為外部身份驗證(ExternalAuthentication), 區別於Abp的外部身份授權(ExternalAuth),這裡Auth的全稱應為Authorization,即授權。
首先來釐清這兩個不同的業務在Abp中的實現,我之前寫的這篇 Abp.Zero 搭建第三方登錄模塊 系列文章中描述的業務,即使用的Abp外部身份授權(ExternalAuth)的相關擴展而實現的。還記得我們實現的WeChatAuthProvider嗎?它繼承於ExternalAuthProviderApi這個抽象類,實現的微信授權功能。所以微信登錄這個動作,實際是在授權(Authorization)已有的微信賬號,訪問服務端資源,而身份驗證(Authentication)步驟,已在其他端完成了(手機微信掃碼),在服務端獲取已驗證好身份的第三方賬戶並生成Token則可以抽象的認為是授權(Authorization)行為。
所以「搭建第三方登錄模塊」應該更準確地描述為「第三方授權模塊」。
從Abp接口設計上,也能看得出來兩者的差別。
外部身份驗證(ExternalAuthentication)關注的是校驗,實現TryAuthenticateAsync並返回是否成功,而CreateUserAsync和UpdateUserAsync僅是校驗流程里的一部分,不實現它並不影響身份驗證結果,外部授權源的接口定義如下,
public interface IExternalAuthenticationSource<TTenant, TUser> where TTenant : AbpTenant<TUser> where TUser : AbpUserBase
{
...
Task<bool> TryAuthenticateAsync(string userNameOrEmailAddress, string plainPassword, TTenant tenant);
Task<TUser> CreateUserAsync(string userNameOrEmailAddress, TTenant tenant);
Task UpdateUserAsync(TUser user, TTenant tenant);
}
外部授權(ExternalAuth)這一步關注的業務是拿到外部賬號,如微信的OpenId,所以IExternalAuthManager重點則是GetUserInfo,而IsValidUser並沒有在默認實現中使用到
public interface IExternalAuthManager
{
Task<bool> IsValidUser(string provider, string providerKey, string providerAccessCode);
Task<ExternalAuthUserInfo> GetUserInfo(string provider, string accessCode);
}
然而這些是從LoginManager原本實現看出的,我們可以重寫這個類原本的方法,加入電話號碼的處理邏輯。
在搞清楚這兩個接口後,相信你會對Abp用戶系統的理解更加深刻
短訊獲取驗證碼來校驗,是比較常用的第三方身份驗證方式,今天來做一個手機號碼免密登錄,並且具有綁定/解綁手機號功能的小案例,效果如圖:
示例代碼已經放在了GitHub上:Github:matoapp-samples
用戶驗證碼校驗模塊
首先定義DomainService接口,我們將實現手機驗證碼的發送、驗證碼校驗、解綁手機號、綁定手機號
這4個功能,並且定義用途以校驗行為合法性,和用它來區分短訊模板
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");
}
public const string LOGIN = "LOGIN";
public const string IDENTITY_VERIFICATION = "IDENTITY_VERIFICATION";
public const string BIND_PHONENUMBER = "BIND_PHONENUMBER";
public const string UNBIND_PHONENUMBER = "UNBIND_PHONENUMBER";
定義一個驗證碼Token緩存管理類,以及對應的緩存條目類,用於承載驗證碼的校驗內容
public class SmsCaptchaTokenCache : MemoryCacheBase<SmsCaptchaTokenCacheItem>, ISingletonDependency
{
public SmsCaptchaTokenCache() : base(nameof(SmsCaptchaTokenCache))
{
}
}
緩存條目將存儲電話號碼,用戶Id(非登錄用途)以及用途
public class SmsCaptchaTokenCacheItem
{
public string PhoneNumber { get; set; }
public long UserId { get; set; }
public string Purpose { get; set; }
}
阿里雲和騰訊雲提供了短訊服務Sms,是國內比較常見的短訊服務提供商,不需要自己寫了,網上有大把的封裝好的庫,這裡使用AbpBoilerplate.Sms作為短訊服務庫。
創建短訊驗證碼的領域服務類SmsCaptchaManager並實現ICaptchaManager接口,同時注入短訊服務ISmsService,用戶管理服務UserManager,驗證碼Token緩存管理服務SmsCaptchaTokenCache
public class SmsCaptchaManager : DomainService, ICaptchaManager
{
private readonly ISmsService SmsService;
private readonly UserManager _userManager;
private readonly SmsCaptchaTokenCache captchaTokenCache;
public static TimeSpan TokenCacheDuration = TimeSpan.FromMinutes(5);
public SmsCaptchaManager(ISmsService SmsService,
UserManager userManager,
SmsCaptchaTokenCache captchaTokenCache
)
{
this.SmsService=SmsService;
_userManager=userManager;
this.captchaTokenCache=captchaTokenCache;
}
}
新建SendCaptchaAsync方法,作為短訊發送和緩存Token方法,CommonHelp中的GetRandomCaptchaNumber()用於生成隨機6位驗證碼,發送完畢後,將此驗證碼作為緩存條目的Key值存入
public async Task SendCaptchaAsync(long userId, string phoneNumber, string purpose)
{
var captcha = CommonHelper.GetRandomCaptchaNumber();
var model = new SendSmsRequest();
model.PhoneNumbers= 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"
};
model.TemplateParam= JsonConvert.SerializeObject(new { code = captcha });
var result = await SmsService.SendSmsAsync(model);
if (string.IsNullOrEmpty(result.BizId) && result.Code!="OK")
{
throw new UserFriendlyException("驗證碼發送失敗,錯誤信息:"+result.Message);
}
await captchaTokenCache.SetAsync(captcha, new SmsCaptchaTokenCacheItem()
{
PhoneNumber=phoneNumber,
UserId=userId,
Purpose=purpose
}, absoluteExpireTime: DateTimeOffset.Now.Add(TokenCacheDuration));
}
綁定手機號功能實現
public async Task BindAsync(string token)
{
SmsCaptchaTokenCacheItem currentItem = await GetToken(token);
if (currentItem==null || currentItem.Purpose!=CaptchaPurpose.BIND_PHONENUMBER)
{
throw new UserFriendlyException("驗證碼不正確或已過期");
}
var user = await _userManager.GetUserByIdAsync(currentItem.UserId);
if (user.IsPhoneNumberConfirmed)
{
throw new UserFriendlyException("已綁定手機,請先解綁後再綁定");
}
user.PhoneNumber=currentItem.PhoneNumber;
user.IsPhoneNumberConfirmed=true;
await _userManager.UpdateAsync(user);
await RemoveToken(token);
}
解綁手機號功能實現
public async Task UnbindAsync(string token)
{
SmsCaptchaTokenCacheItem currentItem = await GetToken(token);
if (currentItem==null|| currentItem.Purpose!=CaptchaPurpose.UNBIND_PHONENUMBER)
{
throw new UserFriendlyException("驗證碼不正確或已過期");
}
var user = await _userManager.GetUserByIdAsync(currentItem.UserId);
user.IsPhoneNumberConfirmed=false;
await _userManager.UpdateAsync(user);
await RemoveToken(token);
}
驗證功能實現
public async Task<bool> VerifyCaptchaAsync(string token, string purpose = CaptchaPurpose.IDENTITY_VERIFICATION)
{
SmsCaptchaTokenCacheItem currentItem = await GetToken(token);
if (currentItem==null || currentItem.Purpose!=purpose)
{
return false;
}
await RemoveToken(token);
return true;
}
實際業務中可能還需要Email驗證,我也建立了電子郵箱驗證碼的領域服務類,只不過沒有實現它,動手能力強的讀者可以試着完善這個小案例:)
Api實現
AppService層創建CaptchaAppService.cs,並寫好接口
public class CaptchaAppService : ApplicationService
{
private readonly SmsCaptchaManager captchaManager;
public CaptchaAppService(SmsCaptchaManager captchaManager)
{
this.captchaManager=captchaManager;
}
[HttpPost]
public async Task SendAsync(SendCaptchaInput input)
{
await captchaManager.SendCaptchaAsync(input.UserId, input.PhoneNumber, input.Type);
}
[HttpPost]
public async Task VerifyAsync(VerifyCaptchaInput input)
{
await captchaManager.VerifyCaptchaAsync(input.Token);
}
[HttpPost]
public async Task UnbindAsync(VerifyCaptchaInput input)
{
await captchaManager.UnbindAsync(input.Token);
}
[HttpPost]
public async Task BindAsync(VerifyCaptchaInput input)
{
await captchaManager.BindAsync(input.Token);
}
}
至此我們就完成了驗證碼相關邏輯的接口
下一章將介紹如何重寫Abp默認方法,以集成手機號登錄功能。
注意!不要將本示例作為生產級代碼使用
本示例中,驗證碼校驗的接口並沒有做嚴格加密,6位驗證碼也很容易被破解,因此需要考慮這些安全問題。在實際生產代碼中,驗證的參數常用手機號+驗證碼做哈希運算保證安全。