Browse Source
- 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 pagepull/1220/head
9 changed files with 549 additions and 2 deletions
@ -0,0 +1,6 @@ |
|||
namespace LINGYUN.Abp.Account.Web; |
|||
|
|||
public static class AbpAccountAuthenticationTypes |
|||
{ |
|||
public const string ShouldChangePassword = "Abp.Account.ShouldChangePassword"; |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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" /> |
|||
@ -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 |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -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"); |
|||
} |
|||
}); |
|||
}); |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue