首页 > 其他分享 >Abp vnext 6.0手机号验证码登录

Abp vnext 6.0手机号验证码登录

时间:2023-02-06 18:13:11浏览次数:54  
标签:vnext string await Abp phoneNumber context 6.0 var public

abp vnext6.0之后官方替换了原来的ids4,采用了openIddict的oauth认证框架。最近有一个需求是要做手机号+短信验证码登录,故需要对openiddict的授权流程进行扩展,下面记录流程

1、短信服务

首先需要在云服务提供商注册短信验证服务,我才用的是阿里云的短信服务,而且abp提供了Volo.Abp.Sms.Aliyun的包,做一下相关的配置就行:

Configure<AbpAliyunSmsOptions>(options =>
	{
	   options.AccessKeyId = ".....";
	   options.AccessKeySecret = ".......";
	   options.EndPoint = "dysmsapi.aliyuncs.com";
	});

2、扩展ITokenExtensionGrant

然后扩展abp提供的一个接口:ITokenExtensionGrant,这个接口是一个空接口,用于标记一个扩展的openiddict的认证授权流程;同时我们需要为手机号验证登录的这个流程定义相关的名字,我们把这个名字放到一个静态类中:

public static class SmsTokenExtensionGrantConsts
	{
		public const string GrantType = "phone_verify";

		public const string ParamName = "phone_number";

		public const string TokenName = "phone_verify_code";

		public const string Purpose = "phone_verify";

		public const string SecurityCodeFailed = "SecurityCodeFailed";
	}

下面是ITokenExtensionGrant这个接口的实现,abp提供了AbpOpenIdDictControllerBase这个基类,我们基于这个类重写HandleAsync就可以了:

        [IgnoreAntiforgeryToken]
	[ApiExplorerSettings(IgnoreApi = true)]
	public class SmsTokenController : AbpOpenIdDictControllerBase, ITokenExtensionGrant
	{
		protected IOptions<IdentityOptions> IdentityOptions => LazyServiceProvider.LazyGetRequiredService<IOptions<IdentityOptions>>();
		protected IUniquePhoneNumberIdentityUserRepository UserRepository => LazyServiceProvider.LazyGetRequiredService<IUniquePhoneNumberIdentityUserRepository>();
		protected IdentitySecurityLogManager IdentitySecurityLogManager => LazyServiceProvider.LazyGetRequiredService<IdentitySecurityLogManager>();
		protected IdentityUserManager IdentityUserManager => LazyServiceProvider.LazyGetRequiredService<IdentityUserManager>();
		protected IVerificationCodeManager VerificationCodeManager => LazyServiceProvider.LazyGetRequiredService<IVerificationCodeManager>();

		public string Name => SmsTokenExtensionGrantConsts.GrantType;

		public async virtual Task<IActionResult> HandleAsync(ExtensionGrantContext context)
		{
			LazyServiceProvider = context.HttpContext.RequestServices.GetRequiredService<IAbpLazyServiceProvider>();

			var phoneNumberParam = context.Request.GetParameter(SmsTokenExtensionGrantConsts.ParamName);
			var phoneTokenParam = context.Request.GetParameter(SmsTokenExtensionGrantConsts.TokenName);

			if (!phoneNumberParam.HasValue || !phoneTokenParam.HasValue)
			{
				Logger.LogInformation("Invalid grant type: phone number or token code not found");

				var properties = new AuthenticationProperties(new Dictionary<string, string>
				{
					[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
					[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = L["InvalidGrant:PhoneOrTokenCodeNotFound"]
				});

				return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
			}

			var phoneToken = phoneTokenParam.Value.ToString();
			var phoneNumber = phoneNumberParam.Value.ToString();

			await IdentityOptions.SetAsync();

			var currentUser = await UserRepository.FindByConfirmedPhoneNumberAsync(phoneNumber);

			if (currentUser == null)
			{
				//Logger.LogInformation("Invalid grant type: phone number not register");

				//var properties = new AuthenticationProperties(new Dictionary<string, string>
				//{
				//	[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
				//	[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = L["InvalidGrant:PhoneNumberNotRegister"]
				//});

				//return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

				currentUser = await GenerateIdentityUserByPhoneNumber(phoneNumber);
			}

			if (await UserManager.IsLockedOutAsync(currentUser))
			{
				Logger.LogInformation("Authentication failed for username: {username}, reason: locked out", currentUser.UserName);

				var properties = new AuthenticationProperties(new Dictionary<string, string>
				{
					[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
					[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = L["Volo.Abp.Identity:UserLockedOut"]
				});

				await SaveSecurityLogAsync(context, currentUser, OpenIddictSecurityLogActionConsts.LoginLockedout);

				return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
			}

			var validResult = await VerificationCodeManager.VerifiCodeAsync(phoneNumber, phoneToken);

			if (!validResult)
			{
				Logger.LogWarning("Authentication failed for token: {0}, reason: invalid token", phoneToken);
				string errorDescription;

				var identityResult = await UserManager.AccessFailedAsync(currentUser);
				if (identityResult.Succeeded)
				{
					errorDescription = L["InvalidGrant:PhoneVerifyInvalid"];
				}
				else
				{
					Logger.LogInformation("Authentication failed for username: {username}, reason: access failed", currentUser.UserName);


					errorDescription = identityResult.LocalizeErrors(L);
				}

				await SaveSecurityLogAsync(context, currentUser, SmsTokenExtensionGrantConsts.SecurityCodeFailed);

				var properties = new AuthenticationProperties(new Dictionary<string, string>
				{
					[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
					[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = errorDescription
				});

				return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
			}

			(await UserManager.UpdateSecurityStampAsync(currentUser)).CheckErrors();

			return await SetSuccessResultAsync(context, currentUser);
		}

		protected virtual async Task<IActionResult> SetSuccessResultAsync(ExtensionGrantContext context, IdentityUser user)
		{
			Logger.LogInformation("Credentials validated for username: {username}", user.UserName);

			var principal = await SignInManager.CreateUserPrincipalAsync(user);

			principal.SetScopes(context.Request.GetScopes());
			principal.SetResources(await GetResourcesAsync(context.Request.GetScopes()));

			await SetClaimsDestinationsAsync(principal);

			await SaveSecurityLogAsync(
				context,
				user,
				OpenIddictSecurityLogActionConsts.LoginSucceeded);

			return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
		}

		protected async virtual Task SaveSecurityLogAsync(
			ExtensionGrantContext context,
			IdentityUser user,
			string action)
		{
			var logContext = new IdentitySecurityLogContext
			{
				Identity = OpenIddictSecurityLogIdentityConsts.OpenIddict,
				Action = action,
				UserName = user.UserName,
				ClientId = await FindClientIdAsync(context)
			};
			logContext.WithProperty("GrantType", Name);

			await IdentitySecurityLogManager.SaveAsync(logContext);
		}

		protected virtual Task<string> FindClientIdAsync(ExtensionGrantContext context)
		{
			return Task.FromResult(context.Request.ClientId);
		}

		protected virtual async Task<IdentityUser> GenerateIdentityUserByPhoneNumber(string phoneNumber)
		{
			var identityUser = new IdentityUser(GuidGenerator.Create(), phoneNumber, "[email protected]", CurrentTenant.Id);
			identityUser.SetPhoneNumber(phoneNumber, true);

			(await IdentityUserManager.CreateAsync(identityUser, null, false)).CheckErrors();

			return identityUser;
		}
	}

在上面这个类中,我们定义了Name,也就是这个扩展的这个openiddict的认证流程的名字。

然后,我们需要配置OpenIddictServerBuilder,将这个自定义的认证流程添加进去:

//定义一个扩展方法
public static OpenIddictServerBuilder AllowSmsFlow(this OpenIddictServerBuilder builder)
		{
			return builder.AllowCustomFlow(SmsTokenExtensionGrantConsts.GrantType);
		}
//然后添加它

PreConfigure<OpenIddictServerBuilder>(builder =>
			{
				builder.AllowSmsFlow();
			});

3、实现一个IUserValidator<IdentityUser>来限制手机号重复注册

采用手机号登录的方式需要http://asp.net core identity中的手机号保持唯一,这个需要扩展IUserValidator这个接口来进行检查:

public class UniquePhoneNumberUserValidator : IUserValidator<IdentityUser>
	{
		public const string PhoneNumberStartsWithZeroErrorCode = "PhoneNumberStartsWithZero";
		public const string NonNumericPhoneNumberErrorCode = "NonNumericPhoneNumber";
		public const string DuplicatePhoneNumberErrorCode = "DuplicatePhoneNumber";

		private readonly IUniquePhoneNumberIdentityUserRepository _userRepository;
		public UniquePhoneNumberUserValidator(
			IUniquePhoneNumberIdentityUserRepository userRepository)
		{
			_userRepository = userRepository;
		}

		public virtual async Task<IdentityResult> ValidateAsync(UserManager<IdentityUser> manager, IdentityUser user)
		{
			var errors = new List<IdentityError>();

			var phoneNumber = await manager.GetPhoneNumberAsync(user);

			if (string.IsNullOrWhiteSpace(phoneNumber))
			{
				return IdentityResult.Success;
			}

			CheckNotStartsWithZero(phoneNumber, errors);

			CheckIsNumeric(phoneNumber, errors);

			// PhoneNumber can be duplicated but confirmed PhoneNumber can't.
			if (user.PhoneNumberConfirmed)
			{
				await CheckIsNotDuplicateAsync(phoneNumber, manager, user, errors);
			}

			return errors.Count > 0 ? IdentityResult.Failed(errors.ToArray()) : IdentityResult.Success;
		}

		protected virtual async Task CheckIsNotDuplicateAsync(string phoneNumber, UserManager<IdentityUser> userManager,
			IdentityUser user, List<IdentityError> errors)
		{
			Volo.Abp.Identity.IdentityUser owner = await _userRepository.FindByConfirmedPhoneNumberAsync(phoneNumber);

			if (owner != null &&
				!string.Equals(await userManager.GetUserIdAsync(owner), await userManager.GetUserIdAsync(user)))
			{
				errors.Add(new IdentityError
				{
					Code = DuplicatePhoneNumberErrorCode,
					Description = "手机号以被注册"
				});
			}
		}

		protected virtual void CheckIsNumeric(string phoneNumber, List<IdentityError> errors)
		{
			if (!phoneNumber.All(char.IsDigit))
			{
				errors.Add(new IdentityError
				{
					Code = NonNumericPhoneNumberErrorCode,
					Description = "手机号格式错误"
				});
			}
		}

		protected virtual void CheckNotStartsWithZero(string phoneNumber, List<IdentityError> errors)
		{
			if (phoneNumber.StartsWith("0"))
			{
				errors.Add(new IdentityError
				{
					Code = PhoneNumberStartsWithZeroErrorCode,
					Description = "手机号格式错误"
				});
			}
		}
	}

abp默认的IdentityUserRepository没有提供通过手机号检索IdentityUser的方法,所以我们需要自己写一个接口来实现这个功能,代码就不放了,自己实现一个即可。

此外,需要重写登录和注册页面,将手机号的字段添加到上面,我这里是使用vue进行的客户端登录,就没有重写mvc相关的登录和注册逻辑了。

 

转 https://zhuanlan.zhihu.com/p/582835400

标签:vnext,string,await,Abp,phoneNumber,context,6.0,var,public
From: https://www.cnblogs.com/wl-blog/p/17096323.html

相关文章

  • abp vnext自定义claim
    创建UserClaimsPrincipalFactory工厂在Project.Domain中创建ProjectUserClaimsPrincipalFactoryusingSystem;usingSystem.Collections.Generic;usingSystem.Linq;......
  • ABP的IdentityServer4中使用自定义的claim声明
    ABP的IdentityServer4使用自定义的claim声明,我是想增加一个部门Id,登录用户的中文名称在IdentityServer项目的AbpModule中,context.Services.AddScoped<IProfileServ......
  • abp 自定义token
    如何删除访问令牌中未使用的声明?ABP框架版本: v5.2.2用户界面类型:角度数据库提供者:EFCore分层(MVC)或身份服务器分离(角度):是/否异常消息和堆栈跟踪:重现问......
  • Fix error - processing package linux-headers-6.0.0-kali6-amd64 (--configure)
    Fixerror-processingpackagelinux-headers-6.0.0-kali6-amd64(--configure)Issue:Settinguplinux-headers-6.0.0-kali6-amd64(6.0.12-1kali1).../etc/kernel/......
  • ThinkPHP6.0 模型搜索器的使用
    搜索器用于封装查询条件表达式,必须在模型中定义,只有使用模型操作数据时才能用搜索器。调用搜索器时使用的是数据表字段,可以不用定义搜索器方法,默认是=条件;如果不是数据......
  • elasticsearch-8.6.0 配置文件
    #========================ElasticsearchConfiguration=========================##NOTE:Elasticsearchcomeswithreasonabledefaultsformostsettings.#......
  • 使用kubeadm安装k8s1.26.0笔记2
    一.安装版本Kubeadm使用cni方式安装版本:v1.26.0 二.机器准备1.机器规格本次安装1个master和1个node节点Master:192.168.64.6Node:192.168.64.7规则:CPU:2内存:4G系......
  • ASP.NET Core 6.0 基于模型验证的数据验证
    https://zhuanlan.zhihu.com/p/551581094 1.1、数据验证的场景比较传统的验证方式如下:publicstringTraditionValidation(TestModelmodel){if(string.IsNul......
  • centos8+zabbix6.0LTS搭建笔记
    环境:联网1.配置zabbix官方yum源,并安装zabbix服务(server,web,agent)rpm-Uvhhttps://repo.zabbix.com/zabbix/6.0/rhel/8/x86_64/zabbix-release-6.0-4.el8.noarch.rpmdn......
  • EasyAbp / WeChatManagement Public
    WeChatManagement.MiniProgramsAbp小程序管理模块,提供小程序登录、用户个人信息记录、小程序微信服务器等功能,自动适应微信开放平台规则,与微信第三方平台模块轻松衔接。......