committed by
GitHub
30 changed files with 819 additions and 78 deletions
@ -0,0 +1,54 @@ |
|||||
|
import { useAppConfig } from '@vben/hooks'; |
||||
|
|
||||
|
import { UserManager, WebStorageStateStore } from 'oidc-client-ts'; |
||||
|
|
||||
|
const { authority, audience, clientId, clientSecret } = useAppConfig( |
||||
|
import.meta.env, |
||||
|
import.meta.env.PROD, |
||||
|
); |
||||
|
|
||||
|
const userManager = new UserManager({ |
||||
|
authority, |
||||
|
client_id: clientId, |
||||
|
client_secret: clientSecret, |
||||
|
redirect_uri: `${window.location.origin}/signin-callback`, |
||||
|
response_type: 'code', |
||||
|
scope: audience, |
||||
|
post_logout_redirect_uri: `${window.location.origin}/`, |
||||
|
silent_redirect_uri: `${window.location.origin}/silent-renew.html`, |
||||
|
automaticSilentRenew: true, |
||||
|
loadUserInfo: true, |
||||
|
userStore: new WebStorageStateStore({ store: window.localStorage }), |
||||
|
}); |
||||
|
|
||||
|
export default { |
||||
|
async login() { |
||||
|
return userManager.signinRedirect(); |
||||
|
}, |
||||
|
|
||||
|
async logout() { |
||||
|
return userManager.signoutRedirect(); |
||||
|
}, |
||||
|
|
||||
|
async refreshToken() { |
||||
|
return userManager.signinSilent(); |
||||
|
}, |
||||
|
|
||||
|
async getAccessToken() { |
||||
|
const user = await userManager.getUser(); |
||||
|
return user?.access_token; |
||||
|
}, |
||||
|
|
||||
|
async isAuthenticated() { |
||||
|
const user = await userManager.getUser(); |
||||
|
return !!user && !user.expired; |
||||
|
}, |
||||
|
|
||||
|
async handleCallback() { |
||||
|
return userManager.signinRedirectCallback(); |
||||
|
}, |
||||
|
|
||||
|
async getUser() { |
||||
|
return userManager.getUser(); |
||||
|
}, |
||||
|
}; |
||||
@ -0,0 +1,15 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { onMounted } from 'vue'; |
||||
|
|
||||
|
import { useAuthStore } from '#/store/auth'; |
||||
|
|
||||
|
const authStore = useAuthStore(); |
||||
|
|
||||
|
onMounted(async () => { |
||||
|
await authStore.oidcCallback(); |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div>{{ $t('page.auth.processingLogin') }}</div> |
||||
|
</template> |
||||
@ -1,64 +1,57 @@ |
|||||
using Microsoft.AspNetCore.Authentication; |
using Microsoft.AspNetCore.Authentication; |
||||
using Microsoft.AspNetCore.Mvc; |
using Microsoft.AspNetCore.Mvc; |
||||
using Microsoft.Extensions.Options; |
using Microsoft.Extensions.Options; |
||||
using System.Collections.Generic; |
using System; |
||||
using System.Threading.Tasks; |
using System.Threading.Tasks; |
||||
using Volo.Abp.Account.Web; |
using Volo.Abp.Account.Web; |
||||
using Volo.Abp.Account.Web.Pages.Account; |
|
||||
using Volo.Abp.DependencyInjection; |
using Volo.Abp.DependencyInjection; |
||||
using Volo.Abp.Identity; |
using Volo.Abp.Identity; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
using Volo.Abp.OpenIddict; |
using Volo.Abp.OpenIddict; |
||||
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 |
||||
{ |
{ |
||||
/// <summary>
|
|
||||
/// 重写登录模型,实现双因素登录
|
|
||||
/// </summary>
|
|
||||
[Dependency(ReplaceServices = true)] |
[Dependency(ReplaceServices = true)] |
||||
[ExposeServices(typeof(LoginModel), typeof(OpenIddictSupportedLoginModel))] |
[ExposeServices( |
||||
public class TwoFactorSupportedLoginModel : OpenIddictSupportedLoginModel |
typeof(LINGYUN.Abp.Account.Web.Pages.Account.LoginModel), |
||||
|
typeof(TwoFactorSupportedLoginModel))] |
||||
|
public class TwoFactorSupportedLoginModel : LINGYUN.Abp.Account.Web.Pages.Account.LoginModel |
||||
{ |
{ |
||||
|
protected AbpOpenIddictRequestHelper OpenIddictRequestHelper { get; } |
||||
public TwoFactorSupportedLoginModel( |
public TwoFactorSupportedLoginModel( |
||||
IAuthenticationSchemeProvider schemeProvider, |
IAuthenticationSchemeProvider schemeProvider, |
||||
IOptions<AbpAccountOptions> accountOptions, |
IOptions<AbpAccountOptions> accountOptions, |
||||
IOptions<IdentityOptions> identityOptions, |
IOptions<IdentityOptions> identityOptions, |
||||
IdentityDynamicClaimsPrincipalContributorCache identityDynamicClaimsPrincipalContributorCache, |
IdentityDynamicClaimsPrincipalContributorCache identityDynamicClaimsPrincipalContributorCache, |
||||
AbpOpenIddictRequestHelper openIddictRequestHelper) |
AbpOpenIddictRequestHelper openIddictRequestHelper) |
||||
: base(schemeProvider, accountOptions, identityOptions, identityDynamicClaimsPrincipalContributorCache, openIddictRequestHelper) |
: base(schemeProvider, accountOptions, identityOptions, identityDynamicClaimsPrincipalContributorCache) |
||||
{ |
{ |
||||
|
OpenIddictRequestHelper = openIddictRequestHelper; |
||||
} |
} |
||||
|
|
||||
protected async override Task<List<ExternalProviderModel>> GetExternalProviders() |
public async override Task<IActionResult> OnGetAsync() |
||||
{ |
{ |
||||
var providers = await base.GetExternalProviders(); |
LoginInput = new LoginInputModel(); |
||||
|
|
||||
foreach (var provider in providers) |
var request = await OpenIddictRequestHelper.GetFromReturnUrlAsync(ReturnUrl); |
||||
|
if (request?.ClientId != null) |
||||
{ |
{ |
||||
var localizedDisplayName = L[provider.DisplayName]; |
// TODO: Find a proper cancel way.
|
||||
if (localizedDisplayName.ResourceNotFound) |
// ShowCancelButton = true;
|
||||
{ |
|
||||
localizedDisplayName = L["AuthenticationScheme:" + provider.DisplayName]; |
|
||||
} |
|
||||
|
|
||||
if (!localizedDisplayName.ResourceNotFound) |
LoginInput.UserNameOrEmailAddress = request.LoginHint; |
||||
|
|
||||
|
//TODO: Reference AspNetCore MultiTenancy module and use options to get the tenant key!
|
||||
|
var tenant = request.GetParameter(TenantResolverConsts.DefaultTenantKey)?.ToString(); |
||||
|
if (!string.IsNullOrEmpty(tenant)) |
||||
{ |
{ |
||||
provider.DisplayName = localizedDisplayName.Value; |
CurrentTenant.Change(Guid.Parse(tenant)); |
||||
|
Response.Cookies.Append(TenantResolverConsts.DefaultTenantKey, tenant); |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
return providers; |
return await base.OnGetAsync(); |
||||
} |
|
||||
|
|
||||
protected override Task<IActionResult> TwoFactorLoginResultAsync() |
|
||||
{ |
|
||||
// 重定向双因素认证页面
|
|
||||
return Task.FromResult<IActionResult>(RedirectToPage("SendCode", new |
|
||||
{ |
|
||||
returnUrl = ReturnUrl, |
|
||||
returnUrlHash = ReturnUrlHash, |
|
||||
rememberMe = LoginInput.RememberMe |
|
||||
})); |
|
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,6 @@ |
|||||
|
namespace LINGYUN.Abp.Account.Web; |
||||
|
|
||||
|
public static class AbpAccountAuthenticationTypes |
||||
|
{ |
||||
|
public const string ShouldChangePassword = "Abp.Account.ShouldChangePassword"; |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
namespace LINGYUN.Abp.Account.Web.Bundling; |
||||
|
|
||||
|
public static class AccountBundles |
||||
|
{ |
||||
|
public static class Scripts |
||||
|
{ |
||||
|
public const string Global = "Abp.Account"; |
||||
|
|
||||
|
public const string ChangePassword = Global + ".ChangePassword"; |
||||
|
} |
||||
|
|
||||
|
public static class Styles |
||||
|
{ |
||||
|
public const string Global = "Abp.Account"; |
||||
|
|
||||
|
public const string UserLoginLink = Global + ".UserLoginLink"; |
||||
|
} |
||||
|
} |
||||
@ -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,14 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Account.Web.Bundling; |
||||
|
|
||||
|
public class UserLoginLinkStyleContributor : BundleContributor |
||||
|
{ |
||||
|
public override Task ConfigureBundleAsync(BundleConfigurationContext context) |
||||
|
{ |
||||
|
context.Files.Add("/styles/user-login-link/fix-style.css"); |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
@using Localization.Resources.AbpUi |
||||
|
@using Microsoft.AspNetCore.Mvc.Localization |
||||
|
@using LINGYUN.Abp.Account.Web.Bundling; |
||||
|
@inject IHtmlLocalizer<AbpUiResource> L |
||||
|
|
||||
|
<abp-style-bundle name="@AccountBundles.Styles.UserLoginLink" /> |
||||
|
|
||||
|
<a class="nav-link fix-margin" role="button" href="~/Account/Login">@L["Login"]</a> |
||||
@ -0,0 +1,4 @@ |
|||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
||||
|
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI |
||||
|
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap |
||||
|
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
.fix-margin { |
||||
|
margin: 10px 0; |
||||
|
} |
||||
Loading…
Reference in new issue