Browse Source

Merge pull request #1220 from colinin/support-oidc-login

feat(vben5): add support OIDC login
pull/1238/head
yx lin 1 year ago
committed by GitHub
parent
commit
e193f751f7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      apps/vben5/apps/app-antd/.env.development
  2. 1
      apps/vben5/apps/app-antd/package.json
  3. 21
      apps/vben5/apps/app-antd/src/adapter/request/index.ts
  4. 54
      apps/vben5/apps/app-antd/src/auth/authService.ts
  5. 5
      apps/vben5/apps/app-antd/src/locales/langs/en-US/page.json
  6. 5
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/page.json
  7. 11
      apps/vben5/apps/app-antd/src/router/routes/core.ts
  8. 60
      apps/vben5/apps/app-antd/src/store/auth.ts
  9. 31
      apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue
  10. 26
      apps/vben5/apps/app-antd/src/views/_core/authentication/third-party-login.vue
  11. 15
      apps/vben5/apps/app-antd/src/views/_core/fallback/login-callback.vue
  12. 2
      apps/vben5/packages/effects/hooks/src/use-app-config.ts
  13. 2
      apps/vben5/packages/types/global.d.ts
  14. 1
      apps/vben5/pnpm-workspace.yaml
  15. 53
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/TwoFactorSupportedLoginModel.cs
  16. 6
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountAuthenticationTypes.cs
  17. 32
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs
  18. 18
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/AccountBundles.cs
  19. 17
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/ChangePasswordScriptContributor.cs
  20. 14
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/UserLoginLinkStyleContributor.cs
  21. 5
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/LINGYUN.Abp.Account.Web.csproj
  22. 8
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Modules/Account/Components/Toolbar/UserLoginLink/Default.cshtml
  23. 4
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Modules/_ViewImports.cshtml
  24. 46
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.cshtml
  25. 169
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.cshtml.cs
  26. 21
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ChangePassword.js
  27. 92
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml
  28. 166
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs
  29. 2
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/ProfileManagement/ProfileManagementPageContributor.cs
  30. 3
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/wwwroot/styles/user-login-link/fix-style.css

7
apps/vben5/apps/app-antd/.env.development

@ -15,6 +15,13 @@ VITE_DEVTOOLS=false
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true
# 是否仅允许OIDC登录
VITE_GLOB_ONLY_OIDC=false
# 认证服务器
VITE_GLOB_AUTHORITY="http://127.0.0.1:30001"
# 授权范围
VITE_GLOB_AUDIENCE="openid email address phone profile offline_access lingyun-abp-application"
# 客户端Id
VITE_GLOB_CLIENT_ID=vue-admin-client
# 客户端密钥【生产环境请勿设置此值,建议启用仅允许OIDC登录,将使用授权码类型登录】
VITE_GLOB_CLIENT_SECRET=1q2w3e*

1
apps/vben5/apps/app-antd/package.json

@ -64,6 +64,7 @@
"@vueuse/core": "catalog:",
"ant-design-vue": "catalog:",
"dayjs": "catalog:",
"oidc-client-ts": "catalog:",
"pinia": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:"

21
apps/vben5/apps/app-antd/src/adapter/request/index.ts

@ -5,7 +5,7 @@ import {
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { useOAuthError, useTokenApi } from '@abp/account';
import { useOAuthError } from '@abp/account';
import { useAbpStore } from '@abp/core';
import { requestClient, useWrapperResult } from '@abp/request';
import { message } from 'ant-design-vue';
@ -13,7 +13,6 @@ import { message } from 'ant-design-vue';
import { useAuthStore } from '#/store';
export function initRequestClient() {
const { refreshTokenApi } = useTokenApi();
/**
* 重新认证逻辑
*/
@ -36,19 +35,11 @@ export function initRequestClient() {
* 刷新token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
if (accessStore.refreshToken) {
try {
const { accessToken, tokenType, refreshToken } = await refreshTokenApi({
refreshToken: accessStore.refreshToken,
});
const newToken = `${tokenType} ${accessToken}`;
accessStore.setAccessToken(newToken);
accessStore.setRefreshToken(refreshToken);
return newToken;
} catch {
console.warn('The refresh token has expired or is unavailable.');
}
const authStore = useAuthStore();
try {
return await authStore.refreshSession();
} catch {
console.warn('The refresh token has expired or is unavailable.');
}
return '';
}

54
apps/vben5/apps/app-antd/src/auth/authService.ts

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

5
apps/vben5/apps/app-antd/src/locales/langs/en-US/page.json

@ -4,7 +4,10 @@
"register": "Register",
"codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login",
"forgetPassword": "Forget Password"
"forgetPassword": "Forget Password",
"oidcLogin": "Open Connect",
"oidcLoginMessage": "Please click \"OK\" to jump to the Certification center for login.",
"processingLogin": "Processing Login..."
},
"dashboard": {
"title": "Dashboard",

5
apps/vben5/apps/app-antd/src/locales/langs/zh-CN/page.json

@ -4,7 +4,10 @@
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
"forgetPassword": "忘记密码"
"forgetPassword": "忘记密码",
"oidcLogin": "认证中心登录",
"oidcLoginMessage": "请点击确定跳转认证中心登录",
"processingLogin": "登录成功,正在跳转中..."
},
"dashboard": {
"title": "概览",

11
apps/vben5/apps/app-antd/src/router/routes/core.ts

@ -21,6 +21,17 @@ const fallbackNotFoundRoute: RouteRecordRaw = {
/** 基本路由,这些路由是必须存在的 */
const coreRoutes: RouteRecordRaw[] = [
{
component: () => import('#/views/_core/fallback/login-callback.vue'),
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
title: 'Processing login',
},
name: 'OidcFallback',
path: '/signin-callback',
},
/**
* 根路由
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。

60
apps/vben5/apps/app-antd/src/store/auth.ts

@ -2,7 +2,7 @@ import type { TokenResult } from '@abp/account';
import type { Recordable, UserInfo } from '@vben/types';
import { ref, watch } from 'vue';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
@ -20,11 +20,12 @@ import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia';
import { useAbpConfigApi } from '#/api/core/useAbpConfigApi';
import authService from '#/auth/authService';
import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => {
const { publish } = useEventBus();
const { loginApi } = useTokenApi();
const { loginApi, refreshTokenApi } = useTokenApi();
const { loginApi: qrcodeLoginApi } = useQrCodeLoginApi();
const { loginApi: phoneLoginApi } = usePhoneLoginApi();
const { getUserInfoApi } = useUserInfoApi();
@ -37,16 +38,45 @@ export const useAuthStore = defineStore('auth', () => {
const loginLoading = ref(false);
watch(
() => accessStore.accessToken,
(accessToken) => {
if (accessToken && !loginLoading.value) {
loginLoading.value = true;
fetchUserInfo();
loginLoading.value = false;
async function refreshSession() {
if (await authService.getAccessToken()) {
const user = await authService.refreshToken();
const newToken = `${user?.token_type} ${user?.access_token}`;
accessStore.setAccessToken(newToken);
if (user?.refresh_token) {
accessStore.setRefreshToken(user.refresh_token);
}
},
);
return newToken;
} else {
const { accessToken, tokenType, refreshToken } = await refreshTokenApi({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
refreshToken: accessStore.refreshToken!,
});
const newToken = `${tokenType} ${accessToken}`;
accessStore.setAccessToken(newToken);
accessStore.setRefreshToken(refreshToken);
return newToken;
}
}
async function oidcLogin() {
await authService.login();
}
async function oidcCallback() {
try {
const user = await authService.handleCallback();
return await _loginSuccess({
accessToken: user.access_token,
tokenType: user.token_type,
refreshToken: user.refresh_token ?? '',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expiresIn: user.expires_in!,
});
} finally {
loginLoading.value = false;
}
}
async function qrcodeLogin(
key: string,
@ -96,7 +126,10 @@ export const useAuthStore = defineStore('auth', () => {
async function logout(redirect: boolean = true) {
try {
// await logoutApi();
if (await authService.getAccessToken()) {
accessStore.setAccessToken(null);
await authService.logout();
}
} catch {
// 不做任何处理
}
@ -200,8 +233,11 @@ export const useAuthStore = defineStore('auth', () => {
authLogin,
phoneLogin,
qrcodeLogin,
oidcLogin,
oidcCallback,
fetchUserInfo,
loginLoading,
logout,
refreshSession,
};
});

31
apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue

@ -7,9 +7,11 @@ import type { Recordable } from '@vben/types';
import { computed, nextTick, onMounted, useTemplateRef } from 'vue';
import { AuthenticationLogin, useVbenModal, z } from '@vben/common-ui';
import { useAppConfig } from '@vben/hooks';
import { $t } from '@vben/locales';
import { useAbpStore, useSettings } from '@abp/core';
import { Modal } from 'ant-design-vue';
import { useAbpConfigApi } from '#/api/core/useAbpConfigApi';
import { useAuthStore } from '#/store';
@ -24,6 +26,8 @@ interface LoginInstance {
defineOptions({ name: 'Login' });
const { onlyOidc } = useAppConfig(import.meta.env, import.meta.env.PROD);
const abpStore = useAbpStore();
const authStore = useAuthStore();
@ -34,6 +38,9 @@ const { getConfigApi } = useAbpConfigApi();
const login = useTemplateRef<LoginInstance>('login');
const formSchema = computed((): VbenFormSchema[] => {
if (onlyOidc) {
return [];
}
let schemas: VbenFormSchema[] = [
{
component: 'Input',
@ -75,6 +82,24 @@ const [ShouldChangePasswordModal, changePasswordModalApi] = useVbenModal({
connectedComponent: ShouldChangePassword,
});
async function onInit() {
if (onlyOidc === true) {
setTimeout(() => {
Modal.confirm({
centered: true,
title: $t('page.auth.oidcLogin'),
content: $t('page.auth.oidcLoginMessage'),
maskClosable: false,
closable: false,
cancelButtonProps: {
disabled: true,
},
async onOk() {
await authStore.oidcLogin();
},
});
}, 300);
return;
}
const abpConfig = await getConfigApi();
abpStore.setApplication(abpConfig);
nextTick(() => {
@ -83,6 +108,10 @@ async function onInit() {
});
}
async function onLogin(params: Recordable<any>) {
if (onlyOidc === true) {
await authStore.oidcLogin();
return;
}
try {
await authStore.authLogin(params);
} catch (error) {
@ -115,7 +144,7 @@ onMounted(onInit);
</script>
<template>
<div>
<div v-if="!onlyOidc">
<AuthenticationLogin
ref="login"
:form-schema="formSchema"

26
apps/vben5/apps/app-antd/src/views/_core/authentication/third-party-login.vue

@ -1,12 +1,17 @@
<script setup lang="ts">
import { MdiGithub, MdiGoogle, MdiQqchat, MdiWechat } from '@vben/icons';
import { $t } from '@vben/locales';
import { Button } from 'ant-design-vue';
import { VbenIconButton } from '@vben-core/shadcn-ui';
import { useAuthStore } from '#/store/auth';
defineOptions({
name: 'ThirdPartyLogin',
});
const authStore = useAuthStore();
async function login() {
await authStore.oidcLogin();
}
</script>
<template>
@ -20,18 +25,9 @@ defineOptions({
</div>
<div class="mt-4 flex flex-wrap justify-center">
<VbenIconButton class="mb-3">
<MdiWechat />
</VbenIconButton>
<VbenIconButton class="mb-3">
<MdiQqchat />
</VbenIconButton>
<VbenIconButton class="mb-3">
<MdiGithub />
</VbenIconButton>
<VbenIconButton class="mb-3">
<MdiGoogle />
</VbenIconButton>
<Button block type="primary" ghost @click="login">
{{ $t('page.auth.oidcLogin') }}
</Button>
</div>
</div>
</template>

15
apps/vben5/apps/app-antd/src/views/_core/fallback/login-callback.vue

@ -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>

2
apps/vben5/packages/effects/hooks/src/use-app-config.ts

@ -21,6 +21,7 @@ export function useAppConfig(
VITE_GLOB_AUDIENCE,
VITE_GLOB_CLIENT_ID,
VITE_GLOB_CLIENT_SECRET,
VITE_GLOB_ONLY_OIDC,
VITE_GLOB_UI_FRAMEWORK,
} = config;
@ -30,6 +31,7 @@ export function useAppConfig(
audience: VITE_GLOB_AUDIENCE,
clientId: VITE_GLOB_CLIENT_ID,
clientSecret: VITE_GLOB_CLIENT_SECRET,
onlyOidc: VITE_GLOB_ONLY_OIDC === 'true',
uiFramework: VITE_GLOB_UI_FRAMEWORK,
};
}

2
apps/vben5/packages/types/global.d.ts

@ -13,6 +13,7 @@ export interface VbenAdminProAppConfigRaw {
VITE_GLOB_CLIENT_SECRET: string;
VITE_GLOB_AUTHORITY: string;
VITE_GLOB_AUDIENCE?: string;
VITE_GLOB_ONLY_OIDC?: string;
VITE_GLOB_UI_FRAMEWORK: string;
}
@ -22,6 +23,7 @@ export interface ApplicationConfig {
audience?: string;
clientId: string;
clientSecret: string;
onlyOidc?: boolean;
uiFramework: string;
}

1
apps/vben5/pnpm-workspace.yaml

@ -134,6 +134,7 @@ catalog:
naive-ui: ^2.41.0
nitropack: ^2.10.4
nprogress: ^0.2.0
oidc-client-ts: ^3.2.1
ora: ^8.2.0
pinia: ^2.3.1
pinia-plugin-persistedstate: ^4.2.0

53
aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/TwoFactorSupportedLoginModel.cs

@ -1,64 +1,57 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System;
using System.Threading.Tasks;
using Volo.Abp.Account.Web;
using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.OpenIddict;
using IdentityOptions = Microsoft.AspNetCore.Identity.IdentityOptions;
namespace LINGYUN.Abp.Account.Web.OpenIddict.Pages.Account
{
/// <summary>
/// 重写登录模型,实现双因素登录
/// </summary>
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(LoginModel), typeof(OpenIddictSupportedLoginModel))]
public class TwoFactorSupportedLoginModel : OpenIddictSupportedLoginModel
[ExposeServices(
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(
IAuthenticationSchemeProvider schemeProvider,
IOptions<AbpAccountOptions> accountOptions,
IOptions<IdentityOptions> identityOptions,
IdentityDynamicClaimsPrincipalContributorCache identityDynamicClaimsPrincipalContributorCache,
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];
if (localizedDisplayName.ResourceNotFound)
{
localizedDisplayName = L["AuthenticationScheme:" + provider.DisplayName];
}
// TODO: Find a proper cancel way.
// ShowCancelButton = true;
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;
}
protected override Task<IActionResult> TwoFactorLoginResultAsync()
{
// 重定向双因素认证页面
return Task.FromResult<IActionResult>(RedirectToPage("SendCode", new
{
returnUrl = ReturnUrl,
returnUrlHash = ReturnUrlHash,
rememberMe = LoginInput.RememberMe
}));
return await base.OnGetAsync();
}
}
}

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

18
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/AccountBundles.cs

@ -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";
}
}

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;
}
}

14
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Bundling/UserLoginLinkStyleContributor.cs

@ -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;
}
}

5
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/LINGYUN.Abp.Account.Web.csproj

@ -38,7 +38,6 @@
<ItemGroup>
<PackageReference Include="Volo.Abp.Sms" />
<PackageReference Include="Volo.Abp.Account.Web" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" />
</ItemGroup>
<ItemGroup>
@ -48,4 +47,8 @@
<ProjectReference Include="..\LINGYUN.Abp.Account.Emailing\LINGYUN.Abp.Account.Emailing.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" />
</ItemGroup>
</Project>

8
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Modules/Account/Components/Toolbar/UserLoginLink/Default.cshtml

@ -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>

4
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Modules/_ViewImports.cshtml

@ -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

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)
{

3
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/wwwroot/styles/user-login-link/fix-style.css

@ -0,0 +1,3 @@
.fix-margin {
margin: 10px 0;
}
Loading…
Cancel
Save