diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json index 4c6527801..38ad3e9b9 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json @@ -15,6 +15,8 @@ "DisplayName:EmailVerifyCode": "Mail verification code", "DisplayName:WeChatCode": "Wechat login code", "DisplayName:AuthenticatorCode": "Authenticator Code", + "DisplayName:PhoneNumber": "Phone Number", + "DisplayName:Code": "Code", "TwoFactor": "Two factor authentication", "TwoFactor:Enabled": "TwoFactor Enabled", "TwoFactor:Email": "Email", @@ -51,6 +53,11 @@ "ProfileTab:Authenticator": "Authenticator", "ProfileTab:SecurityLog": "Security Log", "PhoneNumberChangedMessage": "Your mobile number has been successfully changed.", - "SecurityLogs": "Security Logs" + "SecurityLogs": "Security Logs", + "PasswordLogin": "Login with password", + "PhoneNumberLogin": "Login with phone", + "ScanQrCodeLogin": "Login with scan", + "DisplayName:Abp.Account.EnablePhoneNumberLogin": "Authenticate with a phone number", + "Description:Abp.Account.EnablePhoneNumberLogin": "Indicates whether the server allows users to use mobile phone verification codes." } } \ No newline at end of file diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json index 86f3b2b45..896e15146 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json @@ -15,6 +15,8 @@ "DisplayName:EmailVerifyCode": "邮件验证码", "DisplayName:WeChatCode": "微信登录凭证", "DisplayName:AuthenticatorCode": "验证代码", + "DisplayName:PhoneNumber": "手机号码", + "DisplayName:Code": "验证码", "TwoFactor": "双因素身份验证", "TwoFactor:Enabled": "启用双因素认证", "TwoFactor:Email": "邮箱验证", @@ -51,6 +53,11 @@ "ProfileTab:Authenticator": "身份验证程序", "ProfileTab:SecurityLog": "安全日志", "PhoneNumberChangedMessage": "您的手机号码已成功更改.", - "SecurityLogs": "安全日志" + "SecurityLogs": "安全日志", + "PasswordLogin": "密码登录", + "PhoneNumberLogin": "验证码登录", + "ScanQrCodeLogin": "扫码登录", + "DisplayName:Abp.Account.EnablePhoneNumberLogin": "使用手机验证码进行身份验证", + "Description:Abp.Account.EnablePhoneNumberLogin": "表示服务器是否允许用户使用手机验证码进行身份验证。" } } \ No newline at end of file diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerLoginModel.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerLoginModel.cs index f62229941..c54a5e42e 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerLoginModel.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerLoginModel.cs @@ -4,6 +4,7 @@ using IdentityServer4.Services; using IdentityServer4.Stores; using LINGYUN.Abp.Account.Web.ExternalProviders; using LINGYUN.Abp.Account.Web.Models; +using LINGYUN.Abp.Account.Web.Pages.Account; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -18,7 +19,6 @@ using Volo.Abp.Identity; using Volo.Abp.IdentityServer.AspNetIdentity; using Volo.Abp.MultiTenancy; using Volo.Abp.Settings; -using static Volo.Abp.Account.Web.Pages.Account.LoginModel; using IdentityOptions = Microsoft.AspNetCore.Identity.IdentityOptions; namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account @@ -53,7 +53,7 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account public override async Task OnGetAsync() { - LoginInput = new LoginInputModel(); + PasswordLoginInput = new PasswordLoginInputModel(); var context = await Interaction.GetAuthorizationContextAsync(ReturnUrl); @@ -62,7 +62,7 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account // TODO: Find a proper cancel way. // ShowCancelButton = true; - LoginInput.UserNameOrEmailAddress = context.LoginHint; + PasswordLoginInput.UserNameOrEmailAddress = context.LoginHint; //TODO: Reference AspNetCore MultiTenancy module and use options to get the tenant key! var tenant = context.Parameters[TenantResolverConsts.DefaultTenantKey]; @@ -75,7 +75,7 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account if (context?.IdP != null) { - LoginInput.UserNameOrEmailAddress = context.LoginHint; + PasswordLoginInput.UserNameOrEmailAddress = context.LoginHint; ExternalProviders = new[] { new ExternalLoginProviderModel { AuthenticationScheme = context.IdP } }; return Page(); } @@ -107,7 +107,7 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account return Page(); } - public override async Task OnPostAsync(string action) + public override async Task OnPostPasswordLogin(string action) { var context = await Interaction.GetAuthorizationContextAsync(ReturnUrl); if (action == "Cancel") @@ -138,9 +138,9 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account await ReplaceEmailToUsernameOfInputIfNeeds(); var result = await SignInManager.PasswordSignInAsync( - LoginInput.UserNameOrEmailAddress, - LoginInput.Password, - LoginInput.RememberMe, + PasswordLoginInput.UserNameOrEmailAddress, + PasswordLoginInput.Password, + PasswordLoginInput.RememberMe, true ); @@ -148,7 +148,7 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account { Identity = IdentitySecurityLogIdentityConsts.Identity, Action = result.ToIdentitySecurityLogAction(), - UserName = LoginInput.UserNameOrEmailAddress, + UserName = PasswordLoginInput.UserNameOrEmailAddress, ClientId = context?.Client?.ClientId }); @@ -173,8 +173,8 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account } //TODO: Find a way of getting user's id from the logged in user and do not query it again like that! - var user = await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ?? - await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress); + var user = await UserManager.FindByNameAsync(PasswordLoginInput.UserNameOrEmailAddress) ?? + await UserManager.FindByEmailAsync(PasswordLoginInput.UserNameOrEmailAddress); Debug.Assert(user != null, nameof(user) + " != null"); await IdentityServerEvents.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id.ToString(), user.UserName)); //TODO: Use user's name once implemented diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/OpenIddictLoginModel.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/OpenIddictLoginModel.cs index 96bcaed97..bec7814d0 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/OpenIddictLoginModel.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/OpenIddictLoginModel.cs @@ -1,4 +1,5 @@ using LINGYUN.Abp.Account.Web.ExternalProviders; +using LINGYUN.Abp.Account.Web.Pages.Account; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -9,7 +10,6 @@ using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; using Volo.Abp.MultiTenancy; using Volo.Abp.OpenIddict; -using static Volo.Abp.Account.Web.Pages.Account.LoginModel; using IdentityOptions = Microsoft.AspNetCore.Identity.IdentityOptions; namespace LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account @@ -35,7 +35,7 @@ namespace LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account public async override Task OnGetAsync() { - LoginInput = new LoginInputModel(); + PasswordLoginInput = new PasswordLoginInputModel(); var request = await OpenIddictRequestHelper.GetFromReturnUrlAsync(ReturnUrl); if (request?.ClientId != null) @@ -43,7 +43,7 @@ namespace LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account // TODO: Find a proper cancel way. // ShowCancelButton = true; - LoginInput.UserNameOrEmailAddress = request.LoginHint; + PasswordLoginInput.UserNameOrEmailAddress = request.LoginHint; //TODO: Reference AspNetCore MultiTenancy module and use options to get the tenant key! var tenant = request.GetParameter(TenantResolverConsts.DefaultTenantKey)?.ToString(); diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs index 0b2b446b2..827cd6c77 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs @@ -1,6 +1,5 @@ using LINGYUN.Abp.Account.Emailing; using LINGYUN.Abp.Account.Web.Bundling; -using LINGYUN.Abp.Account.Web.Pages.Account; using LINGYUN.Abp.Account.Web.ProfileManagement; using LINGYUN.Abp.Identity; using LINGYUN.Abp.Identity.AspNetCore.QrCode; @@ -15,7 +14,6 @@ using Volo.Abp.Account.Web.ProfileManagement; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Packages.QRCode; -using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Bundling; using Volo.Abp.AutoMapper; using Volo.Abp.Modularity; using Volo.Abp.Sms; @@ -91,31 +89,38 @@ public class AbpAccountWebModule : AbpModule options.ScriptBundles .Configure(typeof(ManageModel).FullName, - configuration => + bundle => { // Client Proxies - configuration.AddFiles("/client-proxies/account-proxy.js"); + bundle.AddFiles("/client-proxies/account-proxy.js"); // Session - configuration.AddFiles("/Pages/Account/Components/ProfileManagementGroup/Session/Index.js"); + bundle.AddFiles("/Pages/Account/Components/ProfileManagementGroup/Session/Index.js"); // Authenticator - configuration.AddFiles("/Pages/Account/Components/ProfileManagementGroup/Authenticator/Index.js"); + bundle.AddFiles("/Pages/Account/Components/ProfileManagementGroup/Authenticator/Index.js"); // SecurityLog - configuration.AddFiles("/Pages/Account/Components/ProfileManagementGroup/SecurityLog/Index.js"); + bundle.AddFiles("/Pages/Account/Components/ProfileManagementGroup/SecurityLog/Index.js"); // TwoFactor - configuration.AddFiles("/Pages/Account/Components/ProfileManagementGroup/TwoFactor/Default.js"); + bundle.AddFiles("/Pages/Account/Components/ProfileManagementGroup/TwoFactor/Default.js"); // QrCode - configuration.AddContributors(typeof(QRCodeScriptContributor)); + bundle.AddContributors(typeof(QRCodeScriptContributor)); }); options.ScriptBundles .Configure(AccountBundles.Scripts.ChangePassword, bundle => { bundle.AddContributors(typeof(ChangePasswordScriptContributor)); }); + options.ScriptBundles + .Configure(typeof(Pages.Account.LoginModel).FullName, bundle => + { + bundle.AddFiles("/client-proxies/account-proxy.js"); + bundle.AddFiles("/client-proxies/qrcode-proxy.js"); + bundle.AddContributors(typeof(QRCodeScriptContributor)); + }); }); } } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Microsoft/AspNetCore/Mvc/ModelBinding/ModelStateExtensions.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Microsoft/AspNetCore/Mvc/ModelBinding/ModelStateExtensions.cs new file mode 100644 index 000000000..4f72500b8 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Microsoft/AspNetCore/Mvc/ModelBinding/ModelStateExtensions.cs @@ -0,0 +1,27 @@ +using System.Linq; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding; + +public static class ModelStateExtensions +{ + public static void RemoveModelErrors(this ModelStateDictionary modelState, string modelName) + { + var keys = modelState.Keys + .Where(k => k.StartsWith(modelName + ".") || k == modelName) + .ToList(); + + foreach (var key in keys) + { + modelState.Remove(key); + } + } + + public static bool IsValidForModel(this ModelStateDictionary modelState, string modelName) + { + modelState.RemoveModelErrors(modelName); + + return modelState.Keys + .Where(k => k.StartsWith(modelName + ".") || k == modelName) + .All(key => modelState[key].ValidationState == ModelValidationState.Valid); + } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml index f69fe2973..cdda0a3d3 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml @@ -1,18 +1,21 @@ @page +@using LINGYUN.Abp.Account.Web.Pages.Account @using Microsoft.AspNetCore.Mvc.Localization @using Volo.Abp.Account.Localization @using Volo.Abp.Account.Settings -@using Volo.Abp.Account.Web.Pages.Account; +@using Volo.Abp.AspNetCore.Mvc.UI.Layout @using Volo.Abp.AspNetCore.Mvc.UI.Theming; @using Volo.Abp.Identity; -@using Volo.Abp.Settings +@using Volo.Abp.Settings; @model LINGYUN.Abp.Account.Web.Pages.Account.LoginModel @inject IHtmlLocalizer L @inject IThemeManager ThemeManager +@inject IPageLayout PageLayout @inject Volo.Abp.Settings.ISettingProvider SettingProvider @{ Layout = ThemeManager.CurrentTheme.GetAccountLayout(); + PageLayout.Content.Title = L["Login"].Value; } @section scripts @@ -22,84 +25,117 @@ } -
-
-

@L["Login"]

- @if (Model.EnableLocalLogin) - { -
-
- - - -
+
diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs index eb331a83a..00998e44b 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs @@ -1,12 +1,16 @@ using LINGYUN.Abp.Account.Web.ExternalProviders; using LINGYUN.Abp.Account.Web.Models; +using LINGYUN.Abp.Identity.QrCode; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Linq; using System.Security.Claims; @@ -15,19 +19,18 @@ using Volo.Abp; using Volo.Abp.Account.Settings; using Volo.Abp.Account.Web; using Volo.Abp.Account.Web.Pages.Account; -using Volo.Abp.DependencyInjection; +using Volo.Abp.Auditing; using Volo.Abp.Identity; using Volo.Abp.Identity.AspNetCore; using Volo.Abp.Reflection; using Volo.Abp.Security.Claims; using Volo.Abp.Settings; using Volo.Abp.Validation; -using static Volo.Abp.Account.Web.Pages.Account.LoginModel; using IdentityUser = Volo.Abp.Identity.IdentityUser; +using IIdentityUserRepository = LINGYUN.Abp.Identity.IIdentityUserRepository; namespace LINGYUN.Abp.Account.Web.Pages.Account; -//[ExposeServices(typeof(Volo.Abp.Account.Web.Pages.Account.LoginModel))] public class LoginModel : AccountPageModel { [HiddenInput] @@ -38,8 +41,18 @@ public class LoginModel : AccountPageModel [BindProperty(SupportsGet = true)] public string ReturnUrlHash { get; set; } - [BindProperty] - public LoginInputModel LoginInput { get; set; } + [HiddenInput] + [BindProperty(SupportsGet = true)] + public LoginType LoginType { get; set; } + + [BindProperty(Name = "PasswordLoginInput")] + public PasswordLoginInputModel PasswordLoginInput { get; set; } + + [BindProperty(Name = "PhoneLoginInput")] + public PhoneLoginInputModel PhoneLoginInput { get; set; } + + [BindProperty(Name = "QrCodeLoginInput")] + public QrCodeLoginInputModel QrCodeLoginInput { get; set; } public bool EnableLocalLogin { get; set; } @@ -50,6 +63,8 @@ public class LoginModel : AccountPageModel public IEnumerable ExternalProviders { get; set; } public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !x.DisplayName.IsNullOrWhiteSpace()); + protected IIdentityUserRepository UserRepository => LazyServiceProvider.LazyGetRequiredService(); + protected IQrCodeLoginProvider QrCodeLoginProvider => LazyServiceProvider.LazyGetRequiredService(); protected IExternalProviderService ExternalProviderService { get; } protected IAuthenticationSchemeProvider SchemeProvider { get; } @@ -71,7 +86,22 @@ public class LoginModel : AccountPageModel public virtual async Task OnGetAsync() { - LoginInput = new LoginInputModel(); + LoginType = LoginType.Password; + PhoneLoginInput = new PhoneLoginInputModel + { + ReturnUrl = ReturnUrl, + ReturnUrlHash = ReturnUrlHash, + }; + QrCodeLoginInput = new QrCodeLoginInputModel + { + ReturnUrl = ReturnUrl, + ReturnUrlHash = ReturnUrlHash, + }; + PasswordLoginInput = new PasswordLoginInputModel + { + ReturnUrl = ReturnUrl, + ReturnUrlHash = ReturnUrlHash, + }; ExternalProviders = await GetExternalProviders(); @@ -85,24 +115,31 @@ public class LoginModel : AccountPageModel return Page(); } - public async virtual Task OnPostAsync(string action) + public async virtual Task OnPostPasswordLogin(string action) { - await CheckLocalLoginAsync(); + LoginType = LoginType.Password; - ValidateModel(); + await CheckLocalLoginAsync(); ExternalProviders = await GetExternalProviders(); EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin); + ModelState.RemoveModelErrors(nameof(PhoneLoginInput)); + ModelState.RemoveModelErrors(nameof(QrCodeLoginInput)); + if (!TryValidateModel(PasswordLoginInput, nameof(PasswordLoginInput))) + { + return Page(); + } + await ReplaceEmailToUsernameOfInputIfNeeds(); await IdentityOptions.SetAsync(); var result = await SignInManager.PasswordSignInAsync( - LoginInput.UserNameOrEmailAddress, - LoginInput.Password, - LoginInput.RememberMe, + PasswordLoginInput.UserNameOrEmailAddress, + PasswordLoginInput.Password, + PasswordLoginInput.RememberMe, true ); @@ -110,7 +147,7 @@ public class LoginModel : AccountPageModel { Identity = IdentitySecurityLogIdentityConsts.Identity, Action = result.ToIdentitySecurityLogAction(), - UserName = LoginInput.UserNameOrEmailAddress + UserName = PasswordLoginInput.UserNameOrEmailAddress }); if (result.RequiresTwoFactor) @@ -134,14 +171,144 @@ public class LoginModel : AccountPageModel } //TODO: Find a way of getting user's id from the logged in user and do not query it again like that! - var user = await GetIdentityUserAsync(LoginInput.UserNameOrEmailAddress); + var user = await GetIdentityUserAsync(PasswordLoginInput.UserNameOrEmailAddress); Debug.Assert(user != null, nameof(user) + " != null"); // Clear the dynamic claims cache. await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); - return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); + return await RedirectSafelyAsync(PasswordLoginInput.ReturnUrl, PasswordLoginInput.ReturnUrlHash); + } + + public async virtual Task OnPostPhoneNumberLogin(string action) + { + LoginType = LoginType.PhoneNumber; + + await CheckLocalLoginAsync(); + + ExternalProviders = await GetExternalProviders(); + + EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin); + + ModelState.RemoveModelErrors(nameof(QrCodeLoginInput)); + ModelState.RemoveModelErrors(nameof(PasswordLoginInput)); + if (!TryValidateModel(PhoneLoginInput, nameof(PhoneLoginInput))) + { + return Page(); + } + + var user = await UserRepository.FindByPhoneNumberAsync(PhoneLoginInput.PhoneNumber); + if (user == null) + { + Logger.LogInformation("the user phone number is not registed!"); + Alerts.Danger(L["InvalidPhoneNumber"]); + return Page(); + } + + var result = await UserManager.VerifyTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider, PhoneLoginInput.Code); + if (!result) + { + Alerts.Danger(L["InvalidVerifyCode"]); + return Page(); + } + + await SignInManager.SignInAsync(user, PhoneLoginInput.RememberMe); + + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() + { + Identity = IdentitySecurityLogIdentityConsts.IdentityTwoFactor, + Action = IdentitySecurityLogActionConsts.LoginSucceeded, + UserName = user.UserName + }); + + // Clear the dynamic claims cache. + await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); + + return await RedirectSafelyAsync(PhoneLoginInput.ReturnUrl, PhoneLoginInput.ReturnUrlHash); + } + + protected virtual void SetTenantCookies(Guid? tenantId = null) + { + if (tenantId.HasValue) + { + Response.Cookies.Append( + "__tenant", + tenantId.ToString(), + new CookieOptions + { + Path = "/", + HttpOnly = false, + IsEssential = true, + Expires = DateTimeOffset.Now.AddYears(10) + } + ); + } + else + { + Response.Cookies.Delete("__tenant"); + } + } + + public async virtual Task OnPostQrCodeLogin(string action) + { + LoginType = LoginType.QrCode; + + await CheckLocalLoginAsync(); + + ExternalProviders = await GetExternalProviders(); + + EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin); + + ModelState.RemoveModelErrors(nameof(PhoneLoginInput)); + ModelState.RemoveModelErrors(nameof(PasswordLoginInput)); + if (!TryValidateModel(QrCodeLoginInput, nameof(QrCodeLoginInput))) + { + return Page(); + } + + var qrCodeInfo = await QrCodeLoginProvider.GetCodeAsync(QrCodeLoginInput.Key); + // άɨûIdΪ + if (qrCodeInfo == null || qrCodeInfo.Token.IsNullOrWhiteSpace() == true) + { + Alerts.Danger(L["QrCode:Invalid"]); + return Page(); + } + + SetTenantCookies(qrCodeInfo.TenantId); + using (CurrentTenant.Change(qrCodeInfo.TenantId)) + { + var user = await UserManager.FindByIdAsync(qrCodeInfo.UserId); + if (user == null) + { + // TODO: û֤Ч? + Alerts.Danger(L["QrCode:Invalid"]); + return Page(); + } + + if (!await UserManager.VerifyUserTokenAsync(user, QrCodeLoginProviderConsts.Name, QrCodeLoginProviderConsts.Purpose, qrCodeInfo.Token)) + { + Alerts.Danger(L["QrCode:Invalid"]); + return Page(); + } + + // TODO: ס¼ + await SignInManager.SignInAsync(user, true); + + await QrCodeLoginProvider.RemoveAsync(QrCodeLoginInput.Key); + + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() + { + Identity = QrCodeLoginProviderConsts.Purpose, + Action = IdentitySecurityLogActionConsts.LoginSucceeded, + UserName = user.UserName + }); + + // Clear the dynamic claims cache. + await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); + + return await RedirectSafelyAsync(QrCodeLoginInput.ReturnUrl, QrCodeLoginInput.ReturnUrlHash); + } } public virtual async Task OnPostExternalLogin(string provider) @@ -265,17 +432,17 @@ public class LoginModel : AccountPageModel // ض˫֤ҳ return Task.FromResult(RedirectToPage("SendCode", new { - returnUrl = ReturnUrl, - returnUrlHash = ReturnUrlHash, - rememberMe = LoginInput.RememberMe + returnUrl = PasswordLoginInput.ReturnUrl, + returnUrlHash = PasswordLoginInput.ReturnUrlHash, + rememberMe = PasswordLoginInput.RememberMe })); } protected virtual async Task GetIdentityUserAsync(string userNameOrEmailAddress) { - return await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ?? - await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress); + return await UserManager.FindByNameAsync(userNameOrEmailAddress) ?? + await UserManager.FindByEmailAsync(userNameOrEmailAddress); } protected async virtual Task> GetExternalProviders() @@ -316,24 +483,24 @@ public class LoginModel : AccountPageModel protected virtual async Task ReplaceEmailToUsernameOfInputIfNeeds() { - if (!ValidationHelper.IsValidEmailAddress(LoginInput.UserNameOrEmailAddress)) + if (!ValidationHelper.IsValidEmailAddress(PasswordLoginInput.UserNameOrEmailAddress)) { return; } - var userByUsername = await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress); + var userByUsername = await UserManager.FindByNameAsync(PasswordLoginInput.UserNameOrEmailAddress); if (userByUsername != null) { return; } - var userByEmail = await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress); + var userByEmail = await UserManager.FindByEmailAsync(PasswordLoginInput.UserNameOrEmailAddress); if (userByEmail == null) { return; } - LoginInput.UserNameOrEmailAddress = userByEmail.UserName; + PasswordLoginInput.UserNameOrEmailAddress = userByEmail.UserName; } protected virtual async Task CheckLocalLoginAsync() @@ -352,8 +519,8 @@ public class LoginModel : AccountPageModel protected async virtual Task HandleUserNotAllowed() { - var notAllowedUser = await GetIdentityUserAsync(LoginInput.UserNameOrEmailAddress); - if (await UserManager.CheckPasswordAsync(notAllowedUser, LoginInput.Password)) + var notAllowedUser = await GetIdentityUserAsync(PasswordLoginInput.UserNameOrEmailAddress); + if (await UserManager.CheckPasswordAsync(notAllowedUser, PasswordLoginInput.Password)) { // û޸ if (notAllowedUser.ShouldChangePasswordOnNextLogin || await UserManager.ShouldPeriodicallyChangePasswordAsync(notAllowedUser)) @@ -369,9 +536,9 @@ public class LoginModel : AccountPageModel return RedirectToPage("ChangePassword", new { - returnUrl = ReturnUrl, - returnUrlHash = ReturnUrlHash, - rememberMe = LoginInput.RememberMe + returnUrl = PasswordLoginInput.ReturnUrl, + returnUrlHash = PasswordLoginInput.ReturnUrlHash, + rememberMe = PasswordLoginInput.RememberMe }); } } @@ -385,3 +552,56 @@ public class LoginModel : AccountPageModel return Task.FromResult(Page()); } } + +public abstract class LoginInputModel +{ + [HiddenInput] + [BindProperty(SupportsGet = true)] + public string ReturnUrl { get; set; } + + [HiddenInput] + [BindProperty(SupportsGet = true)] + public string ReturnUrlHash { get; set; } +} + +public class PhoneLoginInputModel : LoginInputModel +{ + [Phone] + [Required] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))] + public string PhoneNumber { get; set; } + + [Required] + [StringLength(6)] + public string Code { get; set; } + + public bool RememberMe { get; set; } +} + +public class PasswordLoginInputModel : LoginInputModel +{ + [Required] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))] + public string UserNameOrEmailAddress { get; set; } + + [Required] + [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] + [DataType(DataType.Password)] + [DisableAuditing] + public string Password { get; set; } + + public bool RememberMe { get; set; } +} + +public class QrCodeLoginInputModel : LoginInputModel +{ + [HiddenInput] + public string Key { get; set; } +} + +public enum LoginType +{ + Password = 0, + PhoneNumber = 1, + QrCode = 2 +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.js b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.js new file mode 100644 index 000000000..8db8765d1 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.js @@ -0,0 +1,144 @@ +$(function () { + let checkQrCodeTimer; + let isQrCodeInitialized = false; + var qrCodeService = labp.account.qrCodeLogin; + + let sendSmsCodeTimer; + let sendSmsCodeCountDown = 0; + var authService = labp.account.account; + + var l = abp.localization.getResource('AbpAccount'); + var il = abp.localization.getResource('AbpIdentity'); + + $("#SendVerifyCodeButton").click(function (e) { + const button = $(this); + e.preventDefault(); + + var isValid = $('#PhoneNumberForm').validate().element('#PhoneNumberInput'); + if (!isValid) { + return false; + } + + var input = $('#PhoneNumberForm').serializeFormToObject(); + + abp.ui.setBusy({ busy: true }); + authService.sendPhoneSigninCode({ + phoneNumber: input.phoneLoginInput.phoneNumber, + }).then(function () { + abp.ui.clearBusy(); + sendSmsCodeCountDown = 60; + sendSmsCodeTimer = setInterval(function () { + button.prop('disabled', true); + button.text(`${sendSmsCodeCountDown}`); + if (sendSmsCodeCountDown === 0) { + clearInterval(sendSmsCodeTimer); + button.prop('disabled', false); + button.text(l('SendVerifyCode')); + } + sendSmsCodeCountDown--; + }, 1000); + }).catch(function () { + abp.ui.clearBusy(); + }); + }); + + $("#PasswordVisibilityButton").click(function (e) { + let button = $(this); + let passwordInput = button.parent().find("input"); + if (!passwordInput) { + return; + } + + if (passwordInput.attr("type") === "password") { + passwordInput.attr("type", "text"); + } + else { + passwordInput.attr("type", "password"); + } + + let icon = button.find("i"); + if (icon) { + icon.toggleClass("fa-eye-slash").toggleClass("fa-eye"); + } + }); + + $('#LoginFormTabs').on('shown.bs.tab', function (e) { + const tabName = e.target.name; + if (tabName === 'QrCodeLogin') { + initQrCode(); + } else { + releaseQrCodeTimer(); + } + // лtabƳʾ + $('#AbpPageAlerts').remove(); + }); + + function initQrCode() { + if (isQrCodeInitialized) { + return; + } + if (checkQrCodeTimer) { + clearInterval(checkQrCodeTimer); + checkQrCodeTimer = undefined; + } + abp.ui.setBusy({ busy: true }); + qrCodeService.generate().then(function (result) { + abp.ui.clearBusy(); + $('#QrCodeKey').val(result.key); + const qrCodeUrl = 'QRCODE_LOGIN:' + result.key; + $('#QrCode').empty(); + new QRCode(document.getElementById("QrCode"), { + text: qrCodeUrl, + width: 150, + height: 150 + }); + $('#QrCodeStatus').text(il('QrCode:NotScaned')); + checkQrCodeTimer = setInterval(function () { + checkQrCode(result.key); + }, 5000); + }).catch(function () { + abp.ui.clearBusy(); + }); + + isQrCodeInitialized = true; + } + + function checkQrCode(key) { + qrCodeService.check(key, { + abpHandleError: false + }).then(function (result) { + switch (result.status) { + case 10: + releaseQrCodeTimer(); + $('#QrCodeForm').submit(); + break; + case 5: + $('#QrCodeStatus').text(il('QrCode:Scaned')); + // TODO: 滻ûͷ? + if (result.picture) { + $('#QrCode').html('User Avatar'); + } + break; + case 0: + $('#QrCodeStatus').text(il('QrCode:NotScaned')); + break; + case -1: + $('#QrCodeStatus').text(il('QrCode:Invalid')); + releaseQrCodeTimer(); + initQrCode(); + break; + } + }).catch(function () { + console.warn('Check for QR code errors'); + releaseQrCodeTimer(); + }); + } + + function releaseQrCodeTimer() { + if (checkQrCodeTimer) { + clearInterval(checkQrCodeTimer); + checkQrCodeTimer = undefined; + isQrCodeInitialized = false; + } + } +}); diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/wwwroot/client-proxies/qrcode-proxy.js b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/wwwroot/client-proxies/qrcode-proxy.js new file mode 100644 index 000000000..a8272ca3d --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/wwwroot/client-proxies/qrcode-proxy.js @@ -0,0 +1,33 @@ +/* This file is automatically generated by ABP framework to use MVC Controllers from javascript. */ + + +// module account + +(function(){ + + // controller labp.account.qrCodeLogin + + (function(){ + + abp.utils.createNamespace(window, 'labp.account.qrCodeLogin'); + + labp.account.qrCodeLogin.generate = function(ajaxParams) { + return abp.ajax($.extend(true, { + url: abp.appPath + 'api/account/qrcode/generate', + type: 'POST', + dataType: null, + }, ajaxParams)); + }; + + labp.account.qrCodeLogin.check = function(key, ajaxParams) { + return abp.ajax($.extend(true, { + url: abp.appPath + 'api/account/qrcode/' + key + '/check', + type: 'GET' + }, ajaxParams)); + }; + + })(); + +})(); + + diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/Localization/Resources/zh-Hans.json b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/Localization/Resources/zh-Hans.json index f28982da6..fd3174f50 100644 --- a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/Localization/Resources/zh-Hans.json +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/Localization/Resources/zh-Hans.json @@ -2,7 +2,7 @@ "culture": "zh-Hans", "texts": { "QrCode:Invalid": "二维码已失效!", - "QrCode:NotScaned": "未扫描二维码!", + "QrCode:NotScaned": "请扫描二维码!", "QrCode:Scaned": "请确认二维码." } } \ No newline at end of file