Browse Source

feat(account): add QR code login

pull/1313/head
colin 6 months ago
parent
commit
5649da83bc
  1. 3
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json
  2. 3
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json
  3. 22
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerLoginModel.cs
  4. 6
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/OpenIddictLoginModel.cs
  5. 4
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs
  6. 21
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml
  7. 151
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs
  8. 108
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.js
  9. 33
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/wwwroot/client-proxies/qrcode-proxy.js
  10. 2
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/Localization/Resources/zh-Hans.json

3
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:EmailVerifyCode": "Mail verification code",
"DisplayName:WeChatCode": "Wechat login code", "DisplayName:WeChatCode": "Wechat login code",
"DisplayName:AuthenticatorCode": "Authenticator Code", "DisplayName:AuthenticatorCode": "Authenticator Code",
"DisplayName:PhoneNumber": "Phone Number",
"DisplayName:Code": "Code",
"TwoFactor": "Two factor authentication", "TwoFactor": "Two factor authentication",
"TwoFactor:Enabled": "TwoFactor Enabled", "TwoFactor:Enabled": "TwoFactor Enabled",
"TwoFactor:Email": "Email", "TwoFactor:Email": "Email",
@ -54,6 +56,7 @@
"SecurityLogs": "Security Logs", "SecurityLogs": "Security Logs",
"PasswordLogin": "Login with password", "PasswordLogin": "Login with password",
"PhoneNumberLogin": "Login with phone", "PhoneNumberLogin": "Login with phone",
"ScanQrCodeLogin": "Login with scan",
"DisplayName:Abp.Account.EnablePhoneNumberLogin": "Authenticate with a phone number", "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." "Description:Abp.Account.EnablePhoneNumberLogin": "Indicates whether the server allows users to use mobile phone verification codes."
} }

3
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json

@ -15,6 +15,8 @@
"DisplayName:EmailVerifyCode": "邮件验证码", "DisplayName:EmailVerifyCode": "邮件验证码",
"DisplayName:WeChatCode": "微信登录凭证", "DisplayName:WeChatCode": "微信登录凭证",
"DisplayName:AuthenticatorCode": "验证代码", "DisplayName:AuthenticatorCode": "验证代码",
"DisplayName:PhoneNumber": "手机号码",
"DisplayName:Code": "验证码",
"TwoFactor": "双因素身份验证", "TwoFactor": "双因素身份验证",
"TwoFactor:Enabled": "启用双因素认证", "TwoFactor:Enabled": "启用双因素认证",
"TwoFactor:Email": "邮箱验证", "TwoFactor:Email": "邮箱验证",
@ -54,6 +56,7 @@
"SecurityLogs": "安全日志", "SecurityLogs": "安全日志",
"PasswordLogin": "密码登录", "PasswordLogin": "密码登录",
"PhoneNumberLogin": "验证码登录", "PhoneNumberLogin": "验证码登录",
"ScanQrCodeLogin": "扫码登录",
"DisplayName:Abp.Account.EnablePhoneNumberLogin": "使用手机验证码进行身份验证", "DisplayName:Abp.Account.EnablePhoneNumberLogin": "使用手机验证码进行身份验证",
"Description:Abp.Account.EnablePhoneNumberLogin": "表示服务器是否允许用户使用手机验证码进行身份验证。" "Description:Abp.Account.EnablePhoneNumberLogin": "表示服务器是否允许用户使用手机验证码进行身份验证。"
} }

22
aspnet-core/modules/account/LINGYUN.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerLoginModel.cs

@ -4,6 +4,7 @@ using IdentityServer4.Services;
using IdentityServer4.Stores; using IdentityServer4.Stores;
using LINGYUN.Abp.Account.Web.ExternalProviders; using LINGYUN.Abp.Account.Web.ExternalProviders;
using LINGYUN.Abp.Account.Web.Models; using LINGYUN.Abp.Account.Web.Models;
using LINGYUN.Abp.Account.Web.Pages.Account;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -18,7 +19,6 @@ using Volo.Abp.Identity;
using Volo.Abp.IdentityServer.AspNetIdentity; using Volo.Abp.IdentityServer.AspNetIdentity;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings; using Volo.Abp.Settings;
using static Volo.Abp.Account.Web.Pages.Account.LoginModel;
using IdentityOptions = Microsoft.AspNetCore.Identity.IdentityOptions; using IdentityOptions = Microsoft.AspNetCore.Identity.IdentityOptions;
namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account
@ -53,7 +53,7 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account
public override async Task<IActionResult> OnGetAsync() public override async Task<IActionResult> OnGetAsync()
{ {
LoginInput = new LoginInputModel(); PasswordLoginInput = new PasswordLoginInputModel();
var context = await Interaction.GetAuthorizationContextAsync(ReturnUrl); var context = await Interaction.GetAuthorizationContextAsync(ReturnUrl);
@ -62,7 +62,7 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account
// TODO: Find a proper cancel way. // TODO: Find a proper cancel way.
// ShowCancelButton = true; // ShowCancelButton = true;
LoginInput.UserNameOrEmailAddress = context.LoginHint; PasswordLoginInput.UserNameOrEmailAddress = context.LoginHint;
//TODO: Reference AspNetCore MultiTenancy module and use options to get the tenant key! //TODO: Reference AspNetCore MultiTenancy module and use options to get the tenant key!
var tenant = context.Parameters[TenantResolverConsts.DefaultTenantKey]; var tenant = context.Parameters[TenantResolverConsts.DefaultTenantKey];
@ -75,7 +75,7 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account
if (context?.IdP != null) if (context?.IdP != null)
{ {
LoginInput.UserNameOrEmailAddress = context.LoginHint; PasswordLoginInput.UserNameOrEmailAddress = context.LoginHint;
ExternalProviders = new[] { new ExternalLoginProviderModel { AuthenticationScheme = context.IdP } }; ExternalProviders = new[] { new ExternalLoginProviderModel { AuthenticationScheme = context.IdP } };
return Page(); return Page();
} }
@ -107,7 +107,7 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account
return Page(); return Page();
} }
public override async Task<IActionResult> OnPostAsync(string action) public override async Task<IActionResult> OnPostPasswordLogin(string action)
{ {
var context = await Interaction.GetAuthorizationContextAsync(ReturnUrl); var context = await Interaction.GetAuthorizationContextAsync(ReturnUrl);
if (action == "Cancel") if (action == "Cancel")
@ -138,9 +138,9 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account
await ReplaceEmailToUsernameOfInputIfNeeds(); await ReplaceEmailToUsernameOfInputIfNeeds();
var result = await SignInManager.PasswordSignInAsync( var result = await SignInManager.PasswordSignInAsync(
LoginInput.UserNameOrEmailAddress, PasswordLoginInput.UserNameOrEmailAddress,
LoginInput.Password, PasswordLoginInput.Password,
LoginInput.RememberMe, PasswordLoginInput.RememberMe,
true true
); );
@ -148,7 +148,7 @@ namespace LINGYUN.Abp.Account.Web.IdentityServer.Pages.Account
{ {
Identity = IdentitySecurityLogIdentityConsts.Identity, Identity = IdentitySecurityLogIdentityConsts.Identity,
Action = result.ToIdentitySecurityLogAction(), Action = result.ToIdentitySecurityLogAction(),
UserName = LoginInput.UserNameOrEmailAddress, UserName = PasswordLoginInput.UserNameOrEmailAddress,
ClientId = context?.Client?.ClientId 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! //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) ?? var user = await UserManager.FindByNameAsync(PasswordLoginInput.UserNameOrEmailAddress) ??
await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress); await UserManager.FindByEmailAsync(PasswordLoginInput.UserNameOrEmailAddress);
Debug.Assert(user != null, nameof(user) + " != null"); 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 await IdentityServerEvents.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id.ToString(), user.UserName)); //TODO: Use user's name once implemented

6
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.ExternalProviders;
using LINGYUN.Abp.Account.Web.Pages.Account;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -9,7 +10,6 @@ using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity; using Volo.Abp.Identity;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.OpenIddict; using Volo.Abp.OpenIddict;
using static Volo.Abp.Account.Web.Pages.Account.LoginModel;
using IdentityOptions = Microsoft.AspNetCore.Identity.IdentityOptions; using IdentityOptions = Microsoft.AspNetCore.Identity.IdentityOptions;
namespace LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account namespace LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account
@ -35,7 +35,7 @@ namespace LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account
public async override Task<IActionResult> OnGetAsync() public async override Task<IActionResult> OnGetAsync()
{ {
LoginInput = new LoginInputModel(); PasswordLoginInput = new PasswordLoginInputModel();
var request = await OpenIddictRequestHelper.GetFromReturnUrlAsync(ReturnUrl); var request = await OpenIddictRequestHelper.GetFromReturnUrlAsync(ReturnUrl);
if (request?.ClientId != null) if (request?.ClientId != null)
@ -43,7 +43,7 @@ namespace LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account
// TODO: Find a proper cancel way. // TODO: Find a proper cancel way.
// ShowCancelButton = true; // ShowCancelButton = true;
LoginInput.UserNameOrEmailAddress = request.LoginHint; PasswordLoginInput.UserNameOrEmailAddress = request.LoginHint;
//TODO: Reference AspNetCore MultiTenancy module and use options to get the tenant key! //TODO: Reference AspNetCore MultiTenancy module and use options to get the tenant key!
var tenant = request.GetParameter(TenantResolverConsts.DefaultTenantKey)?.ToString(); var tenant = request.GetParameter(TenantResolverConsts.DefaultTenantKey)?.ToString();

4
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs

@ -1,13 +1,11 @@
using LINGYUN.Abp.Account.Emailing; using LINGYUN.Abp.Account.Emailing;
using LINGYUN.Abp.Account.Web.Bundling; using LINGYUN.Abp.Account.Web.Bundling;
using LINGYUN.Abp.Account.Web.Pages.Account;
using LINGYUN.Abp.Account.Web.ProfileManagement; using LINGYUN.Abp.Account.Web.ProfileManagement;
using LINGYUN.Abp.Identity; using LINGYUN.Abp.Identity;
using LINGYUN.Abp.Identity.AspNetCore.QrCode; using LINGYUN.Abp.Identity.AspNetCore.QrCode;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System; using System;
using Volo.Abp.Account.Localization; using Volo.Abp.Account.Localization;
@ -16,7 +14,6 @@ using Volo.Abp.Account.Web.ProfileManagement;
using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.Localization;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.QRCode; using Volo.Abp.AspNetCore.Mvc.UI.Packages.QRCode;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Bundling;
using Volo.Abp.AutoMapper; using Volo.Abp.AutoMapper;
using Volo.Abp.Modularity; using Volo.Abp.Modularity;
using Volo.Abp.Sms; using Volo.Abp.Sms;
@ -121,6 +118,7 @@ public class AbpAccountWebModule : AbpModule
.Configure(typeof(Pages.Account.LoginModel).FullName, bundle => .Configure(typeof(Pages.Account.LoginModel).FullName, bundle =>
{ {
bundle.AddFiles("/client-proxies/account-proxy.js"); bundle.AddFiles("/client-proxies/account-proxy.js");
bundle.AddFiles("/client-proxies/qrcode-proxy.js");
bundle.AddContributors(typeof(QRCodeScriptContributor)); bundle.AddContributors(typeof(QRCodeScriptContributor));
}); });
}); });

21
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml

@ -28,9 +28,11 @@
<div class="account-module-form"> <div class="account-module-form">
@if (Model.EnableLocalLogin) @if (Model.EnableLocalLogin)
{ {
<abp-tabs> <abp-tabs id="LoginFormTabs">
<abp-tab title="@L["PasswordLogin"].Value" active="Model.LoginType == LoginType.Password"> <abp-tab title="@L["PasswordLogin"].Value" active="Model.LoginType == LoginType.Password" header-name="PasswordLogin">
<form method="post" class="mt-4" asp-page-handler="PasswordLogin"> <form method="post" class="mt-4" asp-page-handler="PasswordLogin">
<input asp-for="PasswordLoginInput.ReturnUrl" />
<input asp-for="PasswordLoginInput.ReturnUrlHash" />
<div class="mb-3"> <div class="mb-3">
<label asp-for="PasswordLoginInput.UserNameOrEmailAddress" class="form-label"></label> <label asp-for="PasswordLoginInput.UserNameOrEmailAddress" class="form-label"></label>
<input asp-for="PasswordLoginInput.UserNameOrEmailAddress" class="form-control" /> <input asp-for="PasswordLoginInput.UserNameOrEmailAddress" class="form-control" />
@ -69,8 +71,10 @@
</div> </div>
</form> </form>
</abp-tab> </abp-tab>
<abp-tab title="@L["PhoneNumberLogin"].Value" active="Model.LoginType == LoginType.PhoneNumber"> <abp-tab title="@L["PhoneNumberLogin"].Value" active="Model.LoginType == LoginType.PhoneNumber" header-name="PhoneNumberLogin">
<form method="post" class="mt-4" asp-page-handler="PhoneNumberLogin" id="PhoneNumberForm"> <form method="post" class="mt-4" asp-page-handler="PhoneNumberLogin" id="PhoneNumberForm">
<input asp-for="PhoneLoginInput.ReturnUrl" />
<input asp-for="PhoneLoginInput.ReturnUrlHash" />
<div class="mb-3"> <div class="mb-3">
<label asp-for="PhoneLoginInput.PhoneNumber" class="form-label"></label> <label asp-for="PhoneLoginInput.PhoneNumber" class="form-label"></label>
<input asp-for="PhoneLoginInput.PhoneNumber" class="form-control" id="PhoneNumberInput" /> <input asp-for="PhoneLoginInput.PhoneNumber" class="form-control" id="PhoneNumberInput" />
@ -99,6 +103,17 @@
</div> </div>
</form> </form>
</abp-tab> </abp-tab>
<abp-tab title="@L["ScanQrCodeLogin"].Value" active="Model.LoginType == LoginType.QrCode" header-name="QrCodeLogin">
<form method="post" class="mt-4" asp-page-handler="QrCodeLogin" id="QrCodeForm">
<input asp-for="QrCodeLoginInput.ReturnUrl" />
<input asp-for="QrCodeLoginInput.ReturnUrlHash" />
<input asp-for="QrCodeLoginInput.Key" id="QrCodeKey" />
<div class="gap-2" style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
<div id="QrCode"></div>
<div id="QrCodeStatus"></div>
</div>
</form>
</abp-tab>
</abp-tabs> </abp-tabs>
} }

151
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs

@ -1,6 +1,8 @@
using LINGYUN.Abp.Account.Web.ExternalProviders; using LINGYUN.Abp.Account.Web.ExternalProviders;
using LINGYUN.Abp.Account.Web.Models; using LINGYUN.Abp.Account.Web.Models;
using LINGYUN.Abp.Identity.QrCode;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
@ -24,7 +26,6 @@ using Volo.Abp.Reflection;
using Volo.Abp.Security.Claims; using Volo.Abp.Security.Claims;
using Volo.Abp.Settings; using Volo.Abp.Settings;
using Volo.Abp.Validation; using Volo.Abp.Validation;
using static Volo.Abp.Account.Web.Pages.Account.LoginModel;
using IdentityUser = Volo.Abp.Identity.IdentityUser; using IdentityUser = Volo.Abp.Identity.IdentityUser;
using IIdentityUserRepository = LINGYUN.Abp.Identity.IIdentityUserRepository; using IIdentityUserRepository = LINGYUN.Abp.Identity.IIdentityUserRepository;
@ -50,6 +51,9 @@ public class LoginModel : AccountPageModel
[BindProperty(Name = "PhoneLoginInput")] [BindProperty(Name = "PhoneLoginInput")]
public PhoneLoginInputModel PhoneLoginInput { get; set; } public PhoneLoginInputModel PhoneLoginInput { get; set; }
[BindProperty(Name = "QrCodeLoginInput")]
public QrCodeLoginInputModel QrCodeLoginInput { get; set; }
public bool EnableLocalLogin { get; set; } public bool EnableLocalLogin { get; set; }
public bool ShowCancelButton { get; set; } public bool ShowCancelButton { get; set; }
@ -60,6 +64,7 @@ public class LoginModel : AccountPageModel
public IEnumerable<ExternalLoginProviderModel> VisibleExternalProviders => ExternalProviders.Where(x => !x.DisplayName.IsNullOrWhiteSpace()); public IEnumerable<ExternalLoginProviderModel> VisibleExternalProviders => ExternalProviders.Where(x => !x.DisplayName.IsNullOrWhiteSpace());
protected IIdentityUserRepository UserRepository => LazyServiceProvider.LazyGetRequiredService<IIdentityUserRepository>(); protected IIdentityUserRepository UserRepository => LazyServiceProvider.LazyGetRequiredService<IIdentityUserRepository>();
protected IQrCodeLoginProvider QrCodeLoginProvider => LazyServiceProvider.LazyGetRequiredService<IQrCodeLoginProvider>();
protected IExternalProviderService ExternalProviderService { get; } protected IExternalProviderService ExternalProviderService { get; }
protected IAuthenticationSchemeProvider SchemeProvider { get; } protected IAuthenticationSchemeProvider SchemeProvider { get; }
@ -82,8 +87,21 @@ public class LoginModel : AccountPageModel
public virtual async Task<IActionResult> OnGetAsync() public virtual async Task<IActionResult> OnGetAsync()
{ {
LoginType = LoginType.Password; LoginType = LoginType.Password;
PhoneLoginInput = new PhoneLoginInputModel(); PhoneLoginInput = new PhoneLoginInputModel
PasswordLoginInput = new PasswordLoginInputModel(); {
ReturnUrl = ReturnUrl,
ReturnUrlHash = ReturnUrlHash,
};
QrCodeLoginInput = new QrCodeLoginInputModel
{
ReturnUrl = ReturnUrl,
ReturnUrlHash = ReturnUrlHash,
};
PasswordLoginInput = new PasswordLoginInputModel
{
ReturnUrl = ReturnUrl,
ReturnUrlHash = ReturnUrlHash,
};
ExternalProviders = await GetExternalProviders(); ExternalProviders = await GetExternalProviders();
@ -108,6 +126,7 @@ public class LoginModel : AccountPageModel
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin); EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
ModelState.RemoveModelErrors(nameof(PhoneLoginInput)); ModelState.RemoveModelErrors(nameof(PhoneLoginInput));
ModelState.RemoveModelErrors(nameof(QrCodeLoginInput));
if (!TryValidateModel(PasswordLoginInput, nameof(PasswordLoginInput))) if (!TryValidateModel(PasswordLoginInput, nameof(PasswordLoginInput)))
{ {
return Page(); return Page();
@ -159,7 +178,7 @@ public class LoginModel : AccountPageModel
// Clear the dynamic claims cache. // Clear the dynamic claims cache.
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId);
return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); return await RedirectSafelyAsync(PasswordLoginInput.ReturnUrl, PasswordLoginInput.ReturnUrlHash);
} }
public async virtual Task<IActionResult> OnPostPhoneNumberLogin(string action) public async virtual Task<IActionResult> OnPostPhoneNumberLogin(string action)
@ -172,6 +191,7 @@ public class LoginModel : AccountPageModel
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin); EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
ModelState.RemoveModelErrors(nameof(QrCodeLoginInput));
ModelState.RemoveModelErrors(nameof(PasswordLoginInput)); ModelState.RemoveModelErrors(nameof(PasswordLoginInput));
if (!TryValidateModel(PhoneLoginInput, nameof(PhoneLoginInput))) if (!TryValidateModel(PhoneLoginInput, nameof(PhoneLoginInput)))
{ {
@ -193,9 +213,7 @@ public class LoginModel : AccountPageModel
return Page(); return Page();
} }
await SignInManager.SignInAsync( await SignInManager.SignInAsync(user, PhoneLoginInput.RememberMe);
user,
PasswordLoginInput.RememberMe);
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{ {
@ -207,7 +225,90 @@ public class LoginModel : AccountPageModel
// Clear the dynamic claims cache. // Clear the dynamic claims cache.
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId);
return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); 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<IActionResult> 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<IActionResult> OnPostExternalLogin(string provider) public virtual async Task<IActionResult> OnPostExternalLogin(string provider)
@ -331,8 +432,8 @@ public class LoginModel : AccountPageModel
// ÖØ¶¨ÏòË«ÒòËØÈÏÖ¤Ò³Ãæ // ÖØ¶¨ÏòË«ÒòËØÈÏÖ¤Ò³Ãæ
return Task.FromResult<IActionResult>(RedirectToPage("SendCode", new return Task.FromResult<IActionResult>(RedirectToPage("SendCode", new
{ {
returnUrl = ReturnUrl, returnUrl = PasswordLoginInput.ReturnUrl,
returnUrlHash = ReturnUrlHash, returnUrlHash = PasswordLoginInput.ReturnUrlHash,
rememberMe = PasswordLoginInput.RememberMe rememberMe = PasswordLoginInput.RememberMe
})); }));
} }
@ -340,8 +441,8 @@ public class LoginModel : AccountPageModel
protected virtual async Task<IdentityUser> GetIdentityUserAsync(string userNameOrEmailAddress) protected virtual async Task<IdentityUser> GetIdentityUserAsync(string userNameOrEmailAddress)
{ {
return await UserManager.FindByNameAsync(PasswordLoginInput.UserNameOrEmailAddress) ?? return await UserManager.FindByNameAsync(userNameOrEmailAddress) ??
await UserManager.FindByEmailAsync(PasswordLoginInput.UserNameOrEmailAddress); await UserManager.FindByEmailAsync(userNameOrEmailAddress);
} }
protected async virtual Task<List<ExternalLoginProviderModel>> GetExternalProviders() protected async virtual Task<List<ExternalLoginProviderModel>> GetExternalProviders()
@ -435,8 +536,8 @@ public class LoginModel : AccountPageModel
return RedirectToPage("ChangePassword", new return RedirectToPage("ChangePassword", new
{ {
returnUrl = ReturnUrl, returnUrl = PasswordLoginInput.ReturnUrl,
returnUrlHash = ReturnUrlHash, returnUrlHash = PasswordLoginInput.ReturnUrlHash,
rememberMe = PasswordLoginInput.RememberMe rememberMe = PasswordLoginInput.RememberMe
}); });
} }
@ -452,7 +553,18 @@ public class LoginModel : AccountPageModel
} }
} }
public class PhoneLoginInputModel 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] [Phone]
[Required] [Required]
@ -461,13 +573,12 @@ public class PhoneLoginInputModel
[Required] [Required]
[StringLength(6)] [StringLength(6)]
[Display(Name = "VerifyCode")]
public string Code { get; set; } public string Code { get; set; }
public bool RememberMe { get; set; } public bool RememberMe { get; set; }
} }
public class PasswordLoginInputModel public class PasswordLoginInputModel : LoginInputModel
{ {
[Required] [Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))] [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))]
@ -482,6 +593,12 @@ public class PasswordLoginInputModel
public bool RememberMe { get; set; } public bool RememberMe { get; set; }
} }
public class QrCodeLoginInputModel : LoginInputModel
{
[HiddenInput]
public string Key { get; set; }
}
public enum LoginType public enum LoginType
{ {
Password = 0, Password = 0,

108
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.js

@ -1,9 +1,15 @@
$(function () { $(function () {
let timer; let checkQrCodeTimer;
let countDown = 0; let isQrCodeInitialized = false;
var l = abp.localization.getResource('AbpAccount'); var qrCodeService = labp.account.qrCodeLogin;
let sendSmsCodeTimer;
let sendSmsCodeCountDown = 0;
var authService = labp.account.account; var authService = labp.account.account;
var l = abp.localization.getResource('AbpAccount');
var il = abp.localization.getResource('AbpIdentity');
$("#SendVerifyCodeButton").click(function (e) { $("#SendVerifyCodeButton").click(function (e) {
const button = $(this); const button = $(this);
e.preventDefault(); e.preventDefault();
@ -15,20 +21,24 @@ $(function () {
var input = $('#PhoneNumberForm').serializeFormToObject(); var input = $('#PhoneNumberForm').serializeFormToObject();
abp.ui.setBusy({ busy: true });
authService.sendPhoneSigninCode({ authService.sendPhoneSigninCode({
phoneNumber: input.phoneLoginInput.phoneNumber, phoneNumber: input.phoneLoginInput.phoneNumber,
}).then(function () { }).then(function () {
countDown = 60; abp.ui.clearBusy();
timer = setInterval(function () { sendSmsCodeCountDown = 60;
sendSmsCodeTimer = setInterval(function () {
button.prop('disabled', true); button.prop('disabled', true);
button.text(`${countDown}`); button.text(`${sendSmsCodeCountDown}`);
if (countDown === 0) { if (sendSmsCodeCountDown === 0) {
clearInterval(timer); clearInterval(sendSmsCodeTimer);
button.prop('disabled', false); button.prop('disabled', false);
button.text(l('SendVerifyCode')); button.text(l('SendVerifyCode'));
} }
countDown--; sendSmsCodeCountDown--;
}, 1000); }, 1000);
}).catch(function () {
abp.ui.clearBusy();
}); });
}); });
@ -51,4 +61,84 @@ $(function () {
icon.toggleClass("fa-eye-slash").toggleClass("fa-eye"); 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('<img src="' + result.picture + '" alt="User Avatar" style="width: 150px; height: 150px; border-radius: 50%;">');
}
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;
}
}
}); });

33
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));
};
})();
})();

2
aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/Localization/Resources/zh-Hans.json

@ -2,7 +2,7 @@
"culture": "zh-Hans", "culture": "zh-Hans",
"texts": { "texts": {
"QrCode:Invalid": "二维码已失效!", "QrCode:Invalid": "二维码已失效!",
"QrCode:NotScaned": "扫描二维码!", "QrCode:NotScaned": "扫描二维码!",
"QrCode:Scaned": "请确认二维码." "QrCode:Scaned": "请确认二维码."
} }
} }
Loading…
Cancel
Save