Browse Source

feat(account): Rewrite the login view page

- Add the view of modifying password during login
- Add modification of password authentication policy for temporary storage of sessions
- Change the name of the contributor to the personal information page
pull/1220/head
colin 8 months ago
parent
commit
18343b9f9d
  1. 6
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountAuthenticationTypes.cs
  2. 32
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs
  3. 17
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/ChangePasswordScriptContributor.cs
  4. 46
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.cshtml
  5. 169
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.cshtml.cs
  6. 21
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.js
  7. 92
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml
  8. 166
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs
  9. 2
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/ProfileManagement/ProfileManagementPageContributor.cs

6
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountAuthenticationTypes.cs

@ -0,0 +1,6 @@
namespace LINGYUN.Abp.Account.Web;
public static class AbpAccountAuthenticationTypes
{
public const string ShouldChangePassword = "Abp.Account.ShouldChangePassword";
}

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

@ -1,14 +1,21 @@
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;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using System;
using Volo.Abp.Account.Localization;
using Volo.Abp.Account.Web.Pages.Account;
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;
@ -53,17 +60,35 @@ public class AbpAccountWebModule : AbpModule
{
options.AddMaps<AbpAccountWebModule>(validate: true);
});
context.Services
.AddAuthentication()
.AddCookie(AbpAccountAuthenticationTypes.ShouldChangePassword, options =>
{
options.LoginPath = new PathString("/Account/Login");
options.ExpireTimeSpan = TimeSpan.FromMinutes(5.0);
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
};
});
}
private void ConfigureProfileManagementPage()
{
Configure<ProfileManagementPageOptions>(options =>
{
options.Contributors.Add(new SessionManagementPageContributor());
options.Contributors.Add(new ProfileManagementPageContributor());
});
Configure<AbpBundlingOptions>(options =>
{
options.StyleBundles
.Add(AccountBundles.Styles.UserLoginLink, bundle =>
{
bundle.AddContributors(typeof(UserLoginLinkStyleContributor));
});
options.ScriptBundles
.Configure(typeof(ManageModel).FullName,
configuration =>
@ -86,6 +111,11 @@ public class AbpAccountWebModule : AbpModule
// QrCode
configuration.AddContributors(typeof(QRCodeScriptContributor));
});
options.ScriptBundles
.Configure(AccountBundles.Scripts.ChangePassword, bundle =>
{
bundle.AddContributors(typeof(ChangePasswordScriptContributor));
});
});
}
}

17
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/ChangePasswordScriptContributor.cs

@ -0,0 +1,17 @@
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery;
using Volo.Abp.Modularity;
namespace LINGYUN.Abp.Account.Web.Bundling;
[DependsOn(typeof(JQueryScriptContributor))]
public class ChangePasswordScriptContributor : BundleContributor
{
public override Task ConfigureBundleAsync(BundleConfigurationContext context)
{
context.Files.Add("/Pages/Account/ChangePassword.js");
return Task.CompletedTask;
}
}

46
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.cshtml

@ -0,0 +1,46 @@
@page
@using Volo.Abp.Account.Localization
@using Volo.Abp.Identity
@using Volo.Abp.Users
@using Microsoft.AspNetCore.Mvc.Localization
@using LINGYUN.Abp.Account.Web.Bundling;
@using LINGYUN.Abp.Account.Web.Pages.Account
@inject IHtmlLocalizer<AccountResource> L
@model LINGYUN.Abp.Account.Web.Pages.Account.ChangePasswordModel
<div class="card mt-3 shadow-sm rounded">
<div class="card-body p-5">
<h4>@L["ChangePassword"]</h4>
<form id="ChangePasswordForm" method="post">
<div class="mb-3">
@if (!Model.HideOldPasswordInput)
{
<label asp-for="Input.CurrentPassword" class="form-label"></label>
<div class="input-group">
<input type="password" class="form-control" autocomplete="new-password" maxlength="@IdentityUserConsts.MaxPasswordLength" asp-for="Input.CurrentPassword" />
<button class="btn btn-secondary password-visibility-button" type="button"><i class="fa fa-eye-slash" aria-hidden="true"></i></button>
</div>
<span asp-validation-for="Input.CurrentPassword"></span>
<br />
}
<label asp-for="Input.NewPassword" class="form-label"></label>
<div class="input-group">
<input type="password" class="form-control" autocomplete="new-password" maxlength="@IdentityUserConsts.MaxPasswordLength" asp-for="Input.NewPassword" />
<button class="btn btn-secondary password-visibility-button" type="button"><i class="fa fa-eye-slash" aria-hidden="true"></i></button>
</div>
<span asp-validation-for="Input.NewPassword"></span><br />
<label asp-for="Input.NewPasswordConfirm" class="form-label"></label>
<div class="input-group">
<input type="password" class="form-control" autocomplete="new-password" maxlength="@IdentityUserConsts.MaxPasswordLength" asp-for="Input.NewPasswordConfirm" />
<button class="btn btn-secondary password-visibility-button" type="button"><i class="fa fa-eye-slash" aria-hidden="true"></i></button>
</div>
<span asp-validation-for="Input.NewPasswordConfirm"></span>
</div>
<abp-button type="submit" button-type="Primary" text="@L["Submit"].Value" />
</form>
</div>
</div>
<abp-script-bundle name="@AccountBundles.Scripts.ChangePassword" />

169
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.cshtml.cs

@ -0,0 +1,169 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System;
using System.ComponentModel.DataAnnotations;
using System.Security.Principal;
using System.Threading.Tasks;
using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp.Auditing;
using Volo.Abp.Identity;
using Volo.Abp.Identity.AspNetCore;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Validation;
namespace LINGYUN.Abp.Account.Web.Pages.Account;
public class UserInfoModel : IMultiTenant
{
public Guid Id { get; set; }
public Guid? TenantId { get; set; }
}
public class ChangePasswordInputModel
{
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:CurrentPassword")]
[DataType(DataType.Password)]
[DisableAuditing]
public string CurrentPassword { get; set; }
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:NewPassword")]
[DataType(DataType.Password)]
[DisableAuditing]
public string NewPassword { get; set; }
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:NewPasswordConfirm")]
[DataType(DataType.Password)]
[DisableAuditing]
public string NewPasswordConfirm { get; set; }
}
public class ChangePasswordModel : AccountPageModel
{
[BindProperty]
public UserInfoModel UserInfo { get; set; }
[BindProperty]
public ChangePasswordInputModel Input { get; set; }
[BindProperty(SupportsGet = true)]
public string ReturnUrl { get; set; }
[BindProperty(SupportsGet = true)]
public string ReturnUrlHash { get; set; }
[BindProperty(SupportsGet = true)]
public bool RememberMe { get; set; }
public bool HideOldPasswordInput { get; set; }
public AbpSignInManager AbpSignInManager => LazyServiceProvider.LazyGetRequiredService<AbpSignInManager>();
public IdentityDynamicClaimsPrincipalContributorCache IdentityDynamicClaimsPrincipalContributorCache => LazyServiceProvider.LazyGetRequiredService<IdentityDynamicClaimsPrincipalContributorCache>();
public async virtual Task<IActionResult> OnGetAsync()
{
Input = new ChangePasswordInputModel();
UserInfo = await GetCurrentUser();
if (UserInfo == null || UserInfo.TenantId != CurrentTenant.Id)
{
await HttpContext.SignOutAsync(AbpAccountAuthenticationTypes.ShouldChangePassword);
return RedirectToPage("/Login", new { ReturnUrl, ReturnUrlHash });
}
HideOldPasswordInput = (await UserManager.GetByIdAsync(UserInfo.Id)).PasswordHash == null;
return Page();
}
public async virtual Task<IActionResult> OnPostAsync()
{
if (Input.CurrentPassword == Input.NewPassword)
{
Alerts.Warning(L["NewPasswordSameAsOld"]);
return Page();
}
var userInfo = await GetCurrentUser();
if (userInfo != null)
{
if (userInfo.TenantId == CurrentTenant.Id)
{
try
{
await IdentityOptions.SetAsync();
var user = await UserManager.GetByIdAsync(userInfo.Id);
if (user.PasswordHash == null)
{
(await UserManager.AddPasswordAsync(user, Input.NewPassword)).CheckErrors();
}
else
{
(await UserManager.ChangePasswordAsync(user, Input.CurrentPassword, Input.NewPassword)).CheckErrors();
}
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
Action = IdentitySecurityLogActionConsts.ChangePassword
});
user.SetShouldChangePasswordOnNextLogin(false);
(await UserManager.UpdateAsync(user)).CheckErrors();
await HttpContext.SignOutAsync(AbpAccountAuthenticationTypes.ShouldChangePassword);
await SignInManager.SignInAsync(user, RememberMe);
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{
Identity = IdentitySecurityLogIdentityConsts.IdentityExternal,
Action = IdentitySecurityLogActionConsts.LoginSucceeded,
UserName = user.UserName
});
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId);
return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash);
}
catch (Exception ex)
{
Alerts.Warning(GetLocalizeExceptionMessage(ex));
return Page();
}
}
}
await HttpContext.SignOutAsync(AbpAccountAuthenticationTypes.ShouldChangePassword);
return RedirectToPage("/Login", new { ReturnUrl, ReturnUrlHash });
}
protected async virtual Task<UserInfoModel> GetCurrentUser()
{
var result = await HttpContext.AuthenticateAsync(AbpAccountAuthenticationTypes.ShouldChangePassword);
var userId = result?.Principal?.FindUserId();
if (!userId.HasValue)
{
return null;
}
var tenantId = result.Principal.FindTenantId();
using (CurrentTenant.Change(tenantId, null))
{
var identityUser = await UserManager.FindByIdAsync(userId.Value.ToString());
return identityUser == null
? null
: new UserInfoModel()
{
Id = identityUser.Id,
TenantId = identityUser.TenantId
};
}
}
}

21
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.js

@ -0,0 +1,21 @@
$(function () {
$(".password-visibility-button").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");
}
});
});

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

@ -0,0 +1,92 @@
@page
@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.Theming;
@using Volo.Abp.Identity;
@using Volo.Abp.Settings
@model LINGYUN.Abp.Account.Web.Pages.Account.LoginModel
@inject IHtmlLocalizer<AccountResource> L
@inject IThemeManager ThemeManager
@inject Volo.Abp.Settings.ISettingProvider SettingProvider
@{
Layout = ThemeManager.CurrentTheme.GetAccountLayout();
}
@section scripts
{
<abp-script-bundle name="@typeof(LoginModel).FullName">
<abp-script src="/Pages/Account/Login.js" />
</abp-script-bundle>
}
<div class="card mt-3 shadow-sm rounded">
<div class="card-body p-5">
<h4>@L["Login"]</h4>
@if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled))
{
<strong>
@L["AreYouANewUser"]
<a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Register"]</a>
</strong>
}
@if (Model.EnableLocalLogin)
{
<form method="post" class="mt-4">
<div class="mb-3">
<label asp-for="LoginInput.UserNameOrEmailAddress" class="form-label"></label>
<input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control" />
<span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="LoginInput.Password" class="form-label"></label>
<div class="input-group">
<input type="password" class="form-control" autocomplete="new-password" maxlength="@IdentityUserConsts.MaxPasswordLength" asp-for="LoginInput.Password" />
<button class="btn btn-secondary" type="button" id="PasswordVisibilityButton"><i class="fa fa-eye-slash" aria-hidden="true"></i></button>
</div>
<span asp-validation-for="LoginInput.Password"></span>
</div>
<abp-row>
<abp-column>
<abp-input asp-for="LoginInput.RememberMe" class="mb-4" />
</abp-column>
<abp-column class="text-end">
<a href="@Url.Page("./ForgotPassword", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})">@L["ForgotPassword"]</a>
</abp-column>
</abp-row>
<div class="d-grid gap-2">
<abp-button type="submit" button-type="Primary" name="Action" value="Login" class="btn-lg mt-3">@L["Login"]</abp-button>
@if (Model.ShowCancelButton)
{
<abp-button type="submit" button-type="Secondary" formnovalidate="formnovalidate" name="Action" value="Cancel" class="btn-lg mt-3">@L["Cancel"]</abp-button>
}
</div>
</form>
}
@if (Model.VisibleExternalProviders.Any())
{
<div class="mt-2">
<h5>@L["OrLoginWith"]</h5>
<form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post">
@foreach (var provider in Model.VisibleExternalProviders)
{
<button type="submit" class="btn btn-primary m-1" name="provider" value="@provider.AuthenticationScheme" title="@L["LogInUsingYourProviderAccount", provider.DisplayName]">@provider.DisplayName</button>
}
</form>
</div>
}
@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any())
{
<div class="alert alert-warning">
<strong>@L["InvalidLoginRequest"]</strong>
@L["ThereAreNoLoginSchemesConfiguredForThisClient"]
</div>
}
</div>
</div>

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

@ -0,0 +1,166 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Volo.Abp.Account.Settings;
using Volo.Abp.Account.Web;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.Identity.AspNetCore;
using Volo.Abp.Security.Claims;
using Volo.Abp.Settings;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
namespace LINGYUN.Abp.Account.Web.Pages.Account;
[ExposeServices(typeof(Volo.Abp.Account.Web.Pages.Account.LoginModel))]
public class LoginModel : Volo.Abp.Account.Web.Pages.Account.LoginModel
{
public LoginModel(
IAuthenticationSchemeProvider schemeProvider,
IOptions<AbpAccountOptions> accountOptions,
IOptions<IdentityOptions> identityOptions,
IdentityDynamicClaimsPrincipalContributorCache identityDynamicClaimsPrincipalContributorCache)
: base(schemeProvider, accountOptions, identityOptions, identityDynamicClaimsPrincipalContributorCache)
{
}
public async override Task<IActionResult> OnPostAsync(string action)
{
await CheckLocalLoginAsync();
ValidateModel();
ExternalProviders = await GetExternalProviders();
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
await ReplaceEmailToUsernameOfInputIfNeeds();
await IdentityOptions.SetAsync();
var result = await SignInManager.PasswordSignInAsync(
LoginInput.UserNameOrEmailAddress,
LoginInput.Password,
LoginInput.RememberMe,
true
);
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
Action = result.ToIdentitySecurityLogAction(),
UserName = LoginInput.UserNameOrEmailAddress
});
if (result.RequiresTwoFactor)
{
return await TwoFactorLoginResultAsync();
}
if (result.IsLockedOut)
{
Alerts.Warning(L["UserLockedOutMessage"]);
return Page();
}
if (result.IsNotAllowed)
{
var notAllowedUser = await GetIdentityUserAsync(LoginInput.UserNameOrEmailAddress);
if (await UserManager.CheckPasswordAsync(notAllowedUser, LoginInput.Password))
{
// 用户必须修改密码
if (notAllowedUser.ShouldChangePasswordOnNextLogin || await UserManager.ShouldPeriodicallyChangePasswordAsync(notAllowedUser))
{
var changePwdIdentity = new ClaimsIdentity(AbpAccountAuthenticationTypes.ShouldChangePassword);
changePwdIdentity.AddClaim(new Claim(AbpClaimTypes.UserId, notAllowedUser.Id.ToString()));
if (notAllowedUser.TenantId.HasValue)
{
changePwdIdentity.AddClaim(new Claim(AbpClaimTypes.TenantId, notAllowedUser.TenantId.ToString()));
}
await HttpContext.SignInAsync(AbpAccountAuthenticationTypes.ShouldChangePassword, new ClaimsPrincipal(changePwdIdentity));
return RedirectToPage("ChangePassword", new
{
returnUrl = ReturnUrl,
returnUrlHash = ReturnUrlHash,
rememberMe = LoginInput.RememberMe
});
}
}
Alerts.Warning(L["LoginIsNotAllowed"]);
return Page();
}
if (!result.Succeeded)
{
Alerts.Danger(L["InvalidUserNameOrPassword"]);
return Page();
}
//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);
Debug.Assert(user != null, nameof(user) + " != null");
// Clear the dynamic claims cache.
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId);
return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash);
}
protected override Task<IActionResult> TwoFactorLoginResultAsync()
{
// 重定向双因素认证页面
return Task.FromResult<IActionResult>(RedirectToPage("SendCode", new
{
returnUrl = ReturnUrl,
returnUrlHash = ReturnUrlHash,
rememberMe = LoginInput.RememberMe
}));
}
protected virtual async Task<IdentityUser> GetIdentityUserAsync(string userNameOrEmailAddress)
{
return await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ??
await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress);
}
protected async override Task<List<ExternalProviderModel>> GetExternalProviders()
{
var schemes = await SchemeProvider.GetAllSchemesAsync();
var providers = schemes
.Where(x => x.DisplayName != null || x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase))
.Select(x => new ExternalProviderModel
{
DisplayName = x.DisplayName,
AuthenticationScheme = x.Name
})
.ToList();
foreach (var provider in providers)
{
var localizedDisplayName = L[provider.DisplayName];
if (localizedDisplayName.ResourceNotFound)
{
localizedDisplayName = L["AuthenticationScheme:" + provider.DisplayName];
}
if (!localizedDisplayName.ResourceNotFound)
{
provider.DisplayName = localizedDisplayName.Value;
}
}
return providers;
}
}

2
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/ProfileManagement/SessionManagementPageContributor.cs → aspnet-core/modules/account/LINGYUN.Abp.Account.Web/ProfileManagement/ProfileManagementPageContributor.cs

@ -10,7 +10,7 @@ using Volo.Abp.Account.Web.ProfileManagement;
namespace LINGYUN.Abp.Account.Web.ProfileManagement;
public class SessionManagementPageContributor : IProfileManagementPageContributor
public class ProfileManagementPageContributor : IProfileManagementPageContributor
{
public virtual Task ConfigureAsync(ProfileManagementPageCreationContext context)
{
Loading…
Cancel
Save