這是一篇系列博文,我將使用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驗證,我也建立了電子郵箱驗證碼的領域服務類,只不過沒有實現它,動手能力強的讀者可以試著完善這個小案例:)
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位驗證碼也很容易被破解,因此需要考慮這些安全問題。在實際生產程式碼中,驗證的引數常用手機號+驗證碼做雜湊運算保證安全。