Abp vnext 6.0手机号验证码登录
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, "fake@financing.com", 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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)