

Abp.Zero 手机号免密登录验证与号码绑定功能的实现(一):验证码模块 - 林晓lx
source link: https://www.cnblogs.com/jevonsflash/p/16848698.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Abp.Zero 手机号免密登录验证与号码绑定功能的实现(一):验证码模块
这是一篇系列博文,我将使用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位验证码也很容易被破解,因此需要考虑这些安全问题。在实际生产代码中,验证的参数常用手机号+验证码做哈希运算保证安全。
Recommend
-
17
群晖 SSH 公钥免密登录 首先 SSH 登录群晖,检测群晖当前用户主目录下是否有 .ssh 文件夹,如果没有使用下列命令创建: mkdir ~/.ssh 2. 使用 vim ~/.ssh/authorize...
-
29
V2EX › Google GV 已绑定真实手机号且获取了 GV 号码的情况下,能否删掉重新选号而不需要再次验证手机?
-
22
2.实现代码 #!/bin/bash #Author:cosann #date:2022/7/21 #description:批量部署SSH免密登录脚本 E_ERROR=65 #传参检测 if [ $# -ne 1 ] then echo -e "Usage:$0 ip_list_file " exit E_ERROR fi #初始化 ip_li...
-
16
V2EX › 问与答 chatgpt 账号怎么更改绑定的号码?
-
20
绑定手机号功能已经开通,请及时绑定您的账号 - 标志情报局 标志情报局诞生于2010年,主要报道一切有关企业品牌形象LOGO的新闻,趣事和背后故事。给品牌形象设计爱好者提供学习、交流和分享的专业平台。经过10...
-
44
香港手机号怎么注册登录百度?绑定登记百度云网盘贴吧步骤 ?????? 如何在百度上注册和登录香港手机号?如何将百度云、网盘和贴吧与手机号绑定?以下链接提供了详细步骤: https://www.chenweiliang.com/cwl-30357.html
-
35
???中国手机号秒注册推特!快速绑定登录教程,省时又方便!? https://www.chenweiliang.com/cwl-30570.html ???想注册推特并用中国手机号登录?这篇教程将教你如何秒速完成注册并绑定!快来省时省力,畅享社交媒体乐趣!?
-
20
?想在Instagram登录却收不到验证码?别担心!试试中国虚拟手机号?,顺利解决登录问题!? https://www.chenweiliang.com/cwl-30742.html 点击链接查看详情。一起畅游社交世界吧!?
-
43
✅ 遇到了注册登录YouTube收不到验证码的问题?别担心! https://www.chenweiliang.com/cwl-30852.html 我们为您提供中国虚拟手机号获取验证码的解决方案,让您畅享视频世界!? 访问上方链接,了解更多详情。一起来解决问题吧!
-
23
? 收不到验证码登录不了YouTube?别担心!中国虚拟手机号为你解锁!? https://www.chenweiliang.com/cwl-30890.html 访问是否链接找到解决方法!? 还记得关注我们哦!
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK