Browse Source

Merge pull request #1499 from colinin/link-account

feat(account): Add support for logging in with link accounts
pull/1506/head
yx lin 3 months ago
committed by GitHub
parent
commit
e7a36849ba
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 42
      apps/vben5/apps/app-antd/src/layouts/basic.vue
  2. 1
      apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json
  3. 1
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json
  4. 39
      apps/vben5/apps/app-antd/src/store/auth.ts
  5. 14
      apps/vben5/apps/app-antd/src/views/_core/authentication/login.vue
  6. 1
      apps/vben5/packages/@abp/account/src/api/index.ts
  7. 54
      apps/vben5/packages/@abp/account/src/api/useLinkUsersApi.ts
  8. 192
      apps/vben5/packages/@abp/account/src/components/UserLinkModal.vue
  9. 1
      apps/vben5/packages/@abp/account/src/components/index.ts
  10. 16
      apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts
  11. 2
      apps/vben5/packages/@abp/account/src/types/index.ts
  12. 14
      apps/vben5/packages/@abp/account/src/types/link-user.ts
  13. 10
      apps/vben5/packages/@abp/account/src/types/token.ts
  14. 25
      apps/vben5/packages/@abp/account/src/utils/auth.ts
  15. 18
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/AbpAccountApplicationContractsModule.cs
  16. 15
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/LinkUserBaseDto.cs
  17. 29
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/LinkUserDto.cs
  18. 15
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/LinkUserInput.cs
  19. 7
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/UnLinkUserInput.cs
  20. 14
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/VerifyLinkTokenInput.cs
  21. 27
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/VerifyLinkUserDto.cs
  22. 5
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/VerifyLinkUserInput.cs
  23. 57
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/IIdentityLinkUserAppService.cs
  24. 1
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/IMyProfileAppService.cs
  25. 13
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json
  26. 13
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/zh-Hans.json
  27. 170
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/IdentityLinkUserAppService.cs
  28. 140
      aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN/Abp/Account/AccountController.cs
  29. 70
      aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN/Abp/Account/IdentityLinkUserController.cs
  30. 20
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Controllers/AuthorizeController.cs
  31. 2
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/SelectAccount.cshtml.cs
  32. 26
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/LinkLogged.cshtml
  33. 106
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/LinkLogged.cshtml.cs
  34. 28
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml
  35. 223
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs
  36. 97
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Register.cshtml.cs
  37. 20
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/SendCode.cshtml.cs
  38. 83
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/VerifyCode.cshtml.cs
  39. 4
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentityErrorCodes.cs
  40. 1
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/en.json
  41. 1
      aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/zh-Hans.json
  42. 1
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.LinkUser/LINGYUN/Abp/OpenIddict/LinkUser/AbpOpenIddictLinkUserModule.cs
  43. 37
      aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.LinkUser/LINGYUN/Abp/OpenIddict/LinkUser/LinkUserAuthorizationHandler.cs

42
apps/vben5/apps/app-antd/src/layouts/basic.vue

@ -1,10 +1,10 @@
<script lang="ts" setup>
import type { NotificationItem } from '@vben/layouts';
import { computed, ref, watch } from 'vue';
import { computed, defineAsyncComponent, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui';
import { useWatermark } from '@vben/hooks';
import { createIconifyIcon } from '@vben/icons';
import {
@ -24,6 +24,7 @@ import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue';
const UserSettingsIcon = createIconifyIcon('tdesign:user-setting');
const UserLinkIcon = createIconifyIcon('material-symbols-light:link');
const notifications = ref<NotificationItem[]>([]);
@ -39,7 +40,21 @@ const showDot = computed(() =>
notifications.value.some((item) => !item.isRead),
);
const [LinkUserModal, linkUserModalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(async () => {
const res = await import('@abp/account');
return res.UserLinkModal;
}),
});
const menus = computed(() => [
{
handler: () => {
linkUserModalApi.open();
},
icon: UserLinkIcon,
text: $t('abp.account.linkAccount'),
},
{
handler: () => {
replace('/account/my-settings');
@ -75,6 +90,28 @@ function handleNoticeClear() {
function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
async function handleLinkUser(token: string) {
const { currentUser, currentTenant } = abpStore.application!;
const extraQueryParams: Record<string, string> = {
LinkUserId: currentUser.id!,
LinkToken: token,
};
if (currentTenant.id) {
extraQueryParams.LinkTenantId = currentTenant.id;
}
// 跳转登录页,交由后端处理
await authStore.oidcLogin({
prompt: 'login',
extraQueryParams,
});
}
async function handleLinkLogin(userId: string, tenantId?: string) {
await authStore.linkUseLogin(userId, tenantId, () => {
window.location.reload();
});
}
watch(
() => preferences.app.watermark,
async (enable) => {
@ -108,6 +145,7 @@ watch(
{{ description }}
</span>
</div>
<LinkUserModal @link="handleLinkUser" @login="handleLinkLogin" />
</template>
<template #notification>
<Notification

1
apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json

@ -79,6 +79,7 @@
},
"account": {
"title": "Account",
"linkAccount": "Link Account",
"settings": {
"title": "My Settings",
"basic": {

1
apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json

@ -79,6 +79,7 @@
},
"account": {
"title": "账户管理",
"linkAccount": "关联账户",
"settings": {
"title": "个人设置",
"basic": {

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

@ -1,4 +1,4 @@
import type { OAuthError, TokenResult } from '@abp/account';
import type { OAuthError, SigninRedirectArgs, TokenResult } from '@abp/account';
import type { Recordable, UserInfo } from '@vben/types';
@ -47,8 +47,8 @@ export const useAuthStore = defineStore('auth', () => {
}
}
async function oidcLogin() {
await oAuthService.login();
async function oidcLogin(args?: SigninRedirectArgs) {
await oAuthService.login(args);
}
async function oidcCallback(onError?: (error: OAuthError) => void) {
@ -114,6 +114,34 @@ export const useAuthStore = defineStore('auth', () => {
}
}
async function linkUseLogin(
userId: string,
tenantId?: string,
onSuccess?: () => Promise<void> | void,
) {
try {
loginLoading.value = true;
const accessToken = await oAuthService.getAccessToken();
const user = await oAuthService.loginByLinkUser({
accessToken,
linkUserId: userId,
linkTenantId: tenantId,
});
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!,
},
onSuccess,
);
} finally {
loginLoading.value = false;
}
}
/**
* 异步处理登录操作
* Asynchronously handle the login process
@ -175,7 +203,9 @@ export const useAuthStore = defineStore('auth', () => {
if (user) {
userInfoRes = user.profile;
}
const abpConfig = await getConfigApi();
const abpConfig = await getConfigApi({
includeLocalizationResources: false,
});
userInfo = {
userId: userInfoRes.sub ?? abpConfig.currentUser.id,
username: userInfoRes.uniqueName ?? abpConfig.currentUser.userName,
@ -269,6 +299,7 @@ export const useAuthStore = defineStore('auth', () => {
oidcLogin,
oidcCallback,
fetchUserInfo,
linkUseLogin,
loginLoading,
logout,
refreshSession,

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

@ -5,6 +5,7 @@ import type { ExtendedFormApi, VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { computed, nextTick, onMounted, useTemplateRef } from 'vue';
import { useRoute } from 'vue-router';
import { AuthenticationLogin, useVbenModal, z } from '@vben/common-ui';
import { useAppConfig } from '@vben/hooks';
@ -30,6 +31,7 @@ const { auth } = useAppConfig(import.meta.env, import.meta.env.PROD);
const abpStore = useAbpStore();
const authStore = useAuthStore();
const route = useRoute();
const { isTrue } = useSettings();
@ -83,6 +85,12 @@ const [ShouldChangePasswordModal, changePasswordModalApi] = useVbenModal({
});
async function onInit() {
if (auth.onlyOidc === true) {
const query: Record<string, string> = {};
Object.keys(route.query).forEach((key) => {
if (typeof route.query[key] === 'string') {
query[key] = route.query[key];
}
});
if (auth.onlyOidcHint) {
setTimeout(() => {
Modal.confirm({
@ -95,16 +103,16 @@ async function onInit() {
disabled: true,
},
async onOk() {
await authStore.oidcLogin();
await authStore.oidcLogin(query);
},
});
}, 300);
} else {
await authStore.oidcLogin();
await authStore.oidcLogin(query);
}
return;
}
const abpConfig = await getConfigApi();
const abpConfig = await getConfigApi({ includeLocalizationResources: false });
abpStore.setApplication(abpConfig);
nextTick(() => {
const formApi = login.value?.getFormApi();

1
apps/vben5/packages/@abp/account/src/api/index.ts

@ -1,5 +1,6 @@
export { useAccountApi } from './useAccountApi';
export { useExternalLoginsApi } from './useExternalLoginsApi';
export { useLinkUsersApi } from './useLinkUsersApi';
export { useMySessionApi } from './useMySessionApi';
export { useProfileApi } from './useProfileApi';
export { useScanQrCodeApi } from './useScanQrCodeApi';

54
apps/vben5/packages/@abp/account/src/api/useLinkUsersApi.ts

@ -0,0 +1,54 @@
import type { ListResultDto } from '@abp/core';
import type { LinkUserDto, UnLinkUserInput } from '../types/link-user';
import { useRequest } from '@abp/request';
export function useLinkUsersApi() {
const { cancel, request } = useRequest();
/**
* 查询关联用户列表
* @returns { Promise<ListResultDto<LinkUserDto>> } 关联用户列表
*/
function getListApi(): Promise<ListResultDto<LinkUserDto>> {
return request<ListResultDto<LinkUserDto>>('api/account/link-users', {
method: 'GET',
});
}
/**
* 生成关联用户Token
*/
function generateLinkTokenApi(): Promise<string> {
return request(`api/account/link-users/generate-link-token`, {
method: 'POST',
});
}
/**
* 生成关联用户登录Token
*/
function generateLinkLoginTokenApi(): Promise<string> {
return request(`api/account/link-users/generate-link-login-token`, {
method: 'POST',
});
}
/**
* 取消用户关联
* @param input 参数
*/
function unLinkApi(input: UnLinkUserInput): Promise<void> {
return request(`api/account/link-users/unlink`, {
method: 'POST',
data: input,
});
}
return {
cancel,
getListApi,
generateLinkTokenApi,
generateLinkLoginTokenApi,
unLinkApi,
};
}

192
apps/vben5/packages/@abp/account/src/components/UserLinkModal.vue

@ -0,0 +1,192 @@
<script setup lang="ts">
import type { VxeGridProps } from '@abp/ui';
import type { LinkUserDto } from '../types';
import { h, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { sortby } from '@abp/core';
import { useMessage, useVbenVxeGrid } from '@abp/ui';
import {
CheckOutlined,
CloseOutlined,
DeleteOutlined,
LoginOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import { Button } from 'ant-design-vue';
import { useLinkUsersApi } from '../api/useLinkUsersApi';
const emit = defineEmits<{
(event: 'link', token: string): void;
(event: 'login', userId: string, tenantId?: string): void;
}>();
const message = useMessage();
const { getListApi, generateLinkTokenApi, unLinkApi } = useLinkUsersApi();
const linkUsers = ref<LinkUserDto[]>([]);
const [Modal, modalApi] = useVbenModal({
async onOpenChange(isOpen) {
if (isOpen) {
await onInit();
}
},
title: $t('AbpAccount.LinkUser'),
});
const gridOptions: VxeGridProps<LinkUserDto> = {
columns: [
{
align: 'left',
field: 'linkUserName',
sortable: true,
title: $t('AbpAccount.DisplayName:LinkUserName'),
},
{
align: 'left',
field: 'directlyLinked',
slots: { default: 'directlyLinked' },
sortable: true,
title: $t('AbpAccount.DisplayName:DirectlyLinked'),
width: 150,
},
{
field: 'actions',
fixed: 'right',
slots: { default: 'actions' },
title: $t('AbpUi.Actions'),
width: 220,
},
],
exportConfig: {},
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page, sort }) => {
let items = sortby(linkUsers.value, sort.field);
if (sort.order === 'desc') {
items = items.toReversed();
}
const result = {
totalCount: linkUsers.value.length,
items: items.slice(
(page.currentPage - 1) * page.pageSize,
page.currentPage * page.pageSize,
),
};
return new Promise((resolve) => {
resolve(result);
});
},
},
response: {
total: 'totalCount',
list: 'items',
},
},
sortConfig: {
remote: true,
allowBtn: true,
},
toolbarConfig: {
custom: true,
export: true,
zoom: true,
},
};
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions,
});
async function onInit() {
const { items } = await getListApi();
linkUsers.value = items;
gridApi.query();
}
async function onCreatLink() {
confirm({
async beforeClose(scope) {
if (scope.isConfirm) {
const token = await generateLinkTokenApi();
emit('link', token);
}
},
centered: true,
content: $t('AbpAccount.LinkAccountWillBeCreateWarning'),
title: $t('AbpUi.AreYouSure'),
});
}
function onLoginByLink(record: LinkUserDto) {
emit('login', record.linkUserId, record.linkTenantId);
modalApi.close();
}
async function onDeleteLink(record: LinkUserDto) {
confirm({
async beforeClose(scope) {
if (scope.isConfirm) {
await unLinkApi({
userId: record.linkUserId,
tenantId: record.linkTenantId,
});
message.success($t('AbpUi.DeletedSuccessfully'));
await onInit();
}
},
centered: true,
content: $t('AbpAccount.LinkAccountWillBeDeleteWarning', [
record.linkUserName,
]),
title: $t('AbpUi.AreYouSure'),
});
}
</script>
<template>
<Modal>
<Grid>
<template #toolbar-tools>
<Button :icon="h(PlusOutlined)" type="primary" @click="onCreatLink">
{{ $t('AbpAccount.LinkUser:New') }}
</Button>
</template>
<template #directlyLinked="{ row }">
<div class="flex flex-row justify-center">
<CheckOutlined v-if="row.directlyLinked" class="text-green-500" />
<CloseOutlined v-else class="text-red-500" />
</div>
</template>
<template #actions="{ row }">
<div class="flex flex-row">
<Button
:icon="h(LoginOutlined)"
block
type="link"
@click="onLoginByLink(row)"
>
{{ $t('AbpAccount.LinkUser:Login') }}
</Button>
<Button
:icon="h(DeleteOutlined)"
block
danger
type="link"
@click="onDeleteLink(row)"
>
{{ $t('AbpUi.Delete') }}
</Button>
</div>
</template>
</Grid>
</Modal>
</template>
<style scoped></style>

1
apps/vben5/packages/@abp/account/src/components/index.ts

@ -1,3 +1,4 @@
export { default as MyProfile } from './MyProfile.vue';
export { default as MySetting } from './MySetting.vue';
export { default as QrCodeLogin } from './QrCodeLogin.vue';
export { default as UserLinkModal } from './UserLinkModal.vue';

16
apps/vben5/packages/@abp/account/src/hooks/useOAuthService.ts

@ -1,4 +1,7 @@
import type { SigninRedirectArgs, SignoutRedirectArgs } from 'oidc-client-ts';
import type {
LinkUserTokenRequest,
PasswordTokenRequestModel,
PhoneNumberTokenRequest,
QrCodeTokenRequest,
@ -7,8 +10,12 @@ import type {
import { userManager } from '../utils/auth';
export function useOAuthService() {
async function login() {
return userManager.signinRedirect();
async function login(args?: SigninRedirectArgs) {
return userManager.signinRedirect(args);
}
async function loginByLinkUser(input: LinkUserTokenRequest) {
return userManager.signinLinkUser(input);
}
async function loginByPassword(input: PasswordTokenRequestModel) {
@ -23,8 +30,8 @@ export function useOAuthService() {
return userManager.signinQrCode(input);
}
async function logout() {
return userManager.signoutRedirect();
async function logout(args?: SignoutRedirectArgs) {
return userManager.signoutRedirect(args);
}
async function revokeTokens() {
@ -55,6 +62,7 @@ export function useOAuthService() {
return {
login,
loginByLinkUser,
loginByPassword,
loginBySmsCode,
loginByQrCode,

2
apps/vben5/packages/@abp/account/src/types/index.ts

@ -1,6 +1,8 @@
export * from './account';
export * from './bind';
export * from './external-logins';
export * from './link-user';
export * from './profile';
export * from './token';
export * from './user';
export type { SigninRedirectArgs } from 'oidc-client-ts';

14
apps/vben5/packages/@abp/account/src/types/link-user.ts

@ -0,0 +1,14 @@
interface LinkUserDto {
directlyLinked: boolean;
linkTenantId?: string;
linkTenantName?: string;
linkUserId: string;
linkUserName: string;
}
interface UnLinkUserInput {
tenantId?: string;
userId: string;
}
export type { LinkUserDto, UnLinkUserInput };

10
apps/vben5/packages/@abp/account/src/types/token.ts

@ -38,6 +38,15 @@ interface PasswordTokenRequestModel {
/** 用户名 */
username: string;
}
/** 关联用户授权请求数据模型 */
interface LinkUserTokenRequest {
/** 当前用户访问令牌 */
accessToken?: string;
/** 关联用户租户Id */
linkTenantId?: string;
/** 关联用户Id */
linkUserId: string;
}
/** 令牌撤销请求数据类型 */
interface RevokeTokenRequest {
/** 令牌 */
@ -92,6 +101,7 @@ interface ShouldChangePasswordError extends OAuthError {
}
export type {
LinkUserTokenRequest,
OAuthError,
OAuthTokenRefreshModel,
OAuthTokenResult,

25
apps/vben5/packages/@abp/account/src/utils/auth.ts

@ -1,6 +1,7 @@
import type { Logger, UserManagerSettings } from 'oidc-client-ts';
import type {
LinkUserTokenRequest,
PasswordTokenRequestModel,
PhoneNumberTokenRequest,
QrCodeTokenRequest,
@ -71,6 +72,28 @@ class AbpUserManager extends UserManager {
params.set('userId', model.userId);
}
}
async signinLinkUser(params: LinkUserTokenRequest) {
const logger = this._logger.create('signinLinkUser');
const body = new URLSearchParams({
LinkUserId: params.linkUserId,
grant_type: 'link_user',
client_id: this.settings.client_id,
});
if (params.accessToken) {
body.set('access_token', params.accessToken);
}
if (params.linkTenantId) {
body.set('LinkTenantId', params.linkTenantId);
}
const client_secret = this.settings.client_secret;
if (client_secret) {
body.set('client_secret', client_secret);
}
this._writeUserId(body, params);
this._writeTenantId(body, params);
this._writeTwoFactorToken(body, params);
return await this._fetchUser(logger, body);
}
async signinQrCode(params: QrCodeTokenRequest) {
const logger = this._logger.create('signinQrCode');
const client_secret = this.settings.client_secret;
@ -89,7 +112,6 @@ class AbpUserManager extends UserManager {
this._writeTwoFactorToken(body, params);
return await this._fetchUser(logger, body);
}
override async signinResourceOwnerCredentials(
params: PasswordTokenRequestModel,
) {
@ -111,7 +133,6 @@ class AbpUserManager extends UserManager {
this._writeChangePasswordToken(body, params);
return await this._fetchUser(logger, body);
}
async signinSmsCode(params: PhoneNumberTokenRequest) {
const logger = this._logger.create('signinSmsCode');
const client_secret = this.settings.client_secret;

18
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/AbpAccountApplicationContractsModule.cs

@ -1,14 +1,14 @@
using Volo.Abp.Account.Localization;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace LINGYUN.Abp.Account;
[DependsOn(
typeof(Volo.Abp.Account.AbpAccountApplicationContractsModule))]
public class AbpAccountApplicationContractsModule : AbpModule
{
namespace LINGYUN.Abp.Account;
[DependsOn(
typeof(Volo.Abp.Account.AbpAccountApplicationContractsModule))]
public class AbpAccountApplicationContractsModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
@ -22,5 +22,5 @@ public class AbpAccountApplicationContractsModule : AbpModule
.Get<AccountResource>()
.AddVirtualJson("/LINGYUN/Abp/Account/Localization/Resources");
});
}
}
}
}

15
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/LinkUserBaseDto.cs

@ -0,0 +1,15 @@
using System;
namespace LINGYUN.Abp.Account.Dto;
public abstract class LinkUserBaseDto
{
/// <summary>
/// 关联用户Id
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// 关联租户Id
/// </summary>
public Guid? TenantId { get; set; }
}

29
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/LinkUserDto.cs

@ -0,0 +1,29 @@
using System;
namespace LINGYUN.Abp.Account.Dto;
/// <summary>
/// 关联用户Dto
/// </summary>
public class LinkUserDto
{
/// <summary>
/// 关联用户Id
/// </summary>
public Guid LinkUserId { get; set; }
/// <summary>
/// 关联用户名
/// </summary>
public string LinkUserName { get; set; }
/// <summary>
/// 关联租户Id
/// </summary>
public Guid? LinkTenantId { get; set; }
/// <summary>
/// 关联租户名
/// </summary>
public string LinkTenantName { get; set; }
/// <summary>
/// 直接关联
/// </summary>
public bool DirectlyLinked { get; set; }
}

15
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/LinkUserInput.cs

@ -0,0 +1,15 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace LINGYUN.Abp.Account.Dto;
/// <summary>
/// 关联用户Dto
/// </summary>
public class LinkUserInput : LinkUserBaseDto
{
/// <summary>
/// 关联用户Token
/// </summary>
[Required]
public string Token { get; set; }
}

7
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/UnLinkUserInput.cs

@ -0,0 +1,7 @@
namespace LINGYUN.Abp.Account.Dto;
/// <summary>
/// 取消关联用户Dto
/// </summary>
public class UnLinkUserInput : LinkUserBaseDto
{
}

14
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/VerifyLinkTokenInput.cs

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace LINGYUN.Abp.Account.Dto;
/// <summary>
/// 验证关联用户TokenDto
/// </summary>
public class VerifyLinkTokenInput : LinkUserBaseDto
{
/// <summary>
/// 关联用户Token
/// </summary>
[Required]
public string Token { get; set; }
}

27
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/VerifyLinkUserDto.cs

@ -0,0 +1,27 @@
using System;
namespace LINGYUN.Abp.Account.Dto;
public class VerifyLinkUserDto
{
/// <summary>
/// 关联用户Id
/// </summary>
public Guid? LinkUserId { get; set; }
/// <summary>
/// 关联用户名
/// </summary>
public string LinkUserName { get; set; }
/// <summary>
/// 关联租户Id
/// </summary>
public Guid? LinkTenantId { get; set; }
/// <summary>
/// 关联租户名
/// </summary>
public string LinkTenantName { get; set; }
/// <summary>
/// 是否已关联
/// </summary>
public bool IsLinked { get; set; }
}

5
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/VerifyLinkUserInput.cs

@ -0,0 +1,5 @@
namespace LINGYUN.Abp.Account.Dto;
public class VerifyLinkUserInput : LinkUserBaseDto
{
}

57
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/IIdentityLinkUserAppService.cs

@ -0,0 +1,57 @@
using LINGYUN.Abp.Account.Dto;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace LINGYUN.Abp.Account;
/// <summary>
/// 关联用户应用服务接口
/// </summary>
public interface IIdentityLinkUserAppService : IApplicationService
{
/// <summary>
/// 关联用户
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task LinkAsync(LinkUserInput input);
/// <summary>
/// 取消关联用户
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task UnlinkAsync(UnLinkUserInput input);
/// <summary>
/// 生成关联用户Token
/// </summary>
/// <returns></returns>
Task<string> GenerateLinkTokenAsync();
/// <summary>
/// 生成关联用户登录Token
/// </summary>
/// <returns></returns>
Task<string> GenerateLinkLoginTokenAsync();
/// <summary>
/// 获取已关联用户列表
/// </summary>
/// <returns></returns>
Task<ListResultDto<LinkUserDto>> GetListAsync();
/// <summary>
/// 验证关联用户
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<VerifyLinkUserDto> VerifyLinkUserAsync(VerifyLinkUserInput input);
/// <summary>
/// 验证关联用户Token
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<bool> VerifyLinkTokenAsync(VerifyLinkTokenInput input);
/// <summary>
/// 验证关联用户登录Token
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<bool> VerifyLinkLoginTokenAsync(VerifyLinkTokenInput input);
}

1
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/IMyProfileAppService.cs

@ -1,5 +1,4 @@
using LINGYUN.Abp.Identity;
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;

13
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Localization/Resources/en.json

@ -17,6 +17,8 @@
"DisplayName:AuthenticatorCode": "Authenticator Code",
"DisplayName:PhoneNumber": "Phone Number",
"DisplayName:Code": "Code",
"DisplayName:LinkUserName": "User Name",
"DisplayName:DirectlyLinked": "Directly Linked",
"TwoFactor": "Two factor authentication",
"TwoFactor:Enabled": "TwoFactor Enabled",
"TwoFactor:Email": "Email",
@ -67,6 +69,15 @@
"CancelBindWarningMessage": "After unbinding, you will not be able to use an external identity. If you log in through this method, your session will be logged out!",
"EmailConfirm": "Email Confirm",
"AvatarChanged": "Avatar Changed",
"ClickToValidation": "Click To Validation"
"ClickToValidation": "Click To Validation",
"LinkUser": "Link User",
"LinkUser:New": "New Link User",
"LinkUser:Login": "Log in with this account",
"LinkAccountWarning": "Please note that you are linking to other accounts, <a href=\"{0}\">Click here</a> to cancel the link login!",
"LinkAccountWillBeCreateWarning": "You will log out from your current account and then log in with another account. After completing the login process, the two accounts will be linked together.",
"LinkAccountWillBeDeleteWarning": "Are you sure you want to delete the associated account \"{0}\"?",
"LinkLogged": "Accounts are linked",
"StayWithCurrentAccount": "Stay with the current account",
"Settings:Identity.Account": "Account"
}
}

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

@ -17,6 +17,8 @@
"DisplayName:AuthenticatorCode": "验证代码",
"DisplayName:PhoneNumber": "手机号码",
"DisplayName:Code": "验证码",
"DisplayName:LinkUserName": "用户名",
"DisplayName:DirectlyLinked": "直接关联",
"TwoFactor": "双因素身份验证",
"TwoFactor:Enabled": "启用双因素认证",
"TwoFactor:Email": "邮箱验证",
@ -67,6 +69,15 @@
"CancelBindWarningMessage": "解除绑定后将无法使用外部身份,如果你通过此方式登录,你的会话将被注销!",
"EmailConfirm": "确认邮件",
"AvatarChanged": "变更头像",
"ClickToValidation": "点击去验证"
"ClickToValidation": "点击去验证",
"LinkUser": "关联账户",
"LinkUser:New": "新关联账户",
"LinkUser:Login": "以该账号登录",
"LinkAccountWarning": "请注意,你正在关联到其他帐户,<a href=\"{0}\">单击此处</a>取消链接关联!",
"LinkAccountWillBeCreateWarning": "您将从当前账户退出,然后用另一个账户登录.完成登录后,两个账户将被关联起来.",
"LinkAccountWillBeDeleteWarning": "您确定要删除关联帐户\"{0}\"吗?",
"LinkLogged": "帐户已经关联",
"StayWithCurrentAccount": "继续使用当前帐户",
"Settings:Identity.Account": "用户账户"
}
}

170
aspnet-core/modules/account/LINGYUN.Abp.Account.Application/LINGYUN/Abp/Account/IdentityLinkUserAppService.cs

@ -0,0 +1,170 @@
using LINGYUN.Abp.Account.Dto;
using Microsoft.AspNetCore.Authorization;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Identity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Users;
namespace LINGYUN.Abp.Account;
[Authorize]
public class IdentityLinkUserAppService : AccountApplicationServiceBase, IIdentityLinkUserAppService
{
protected IdentityLinkUserManager IdentityLinkUserManager { get; }
protected ITenantStore TenantStore { get; }
public IdentityLinkUserAppService(
IdentityLinkUserManager identityLinkUserManager,
ITenantStore tenantStore)
{
IdentityLinkUserManager = identityLinkUserManager;
TenantStore = tenantStore;
}
public async virtual Task<string> GenerateLinkLoginTokenAsync()
{
return await IdentityLinkUserManager.GenerateLinkTokenAsync(
new IdentityLinkUserInfo(CurrentUser.GetId(), CurrentTenant.Id),
LinkUserTokenProviderConsts.LinkUserLoginTokenPurpose);
}
public async virtual Task<string> GenerateLinkTokenAsync()
{
return await IdentityLinkUserManager.GenerateLinkTokenAsync(
new IdentityLinkUserInfo(CurrentUser.GetId(), CurrentTenant.Id),
LinkUserTokenProviderConsts.LinkUserTokenPurpose);
}
public async virtual Task<ListResultDto<LinkUserDto>> GetListAsync()
{
var identityLinkUserDtos = new List<LinkUserDto>();
var identityLinkUserInfo = new IdentityLinkUserInfo(CurrentUser.GetId(), CurrentTenant.Id);
var identityLinkUsers = await IdentityLinkUserManager.GetListAsync(identityLinkUserInfo, includeIndirect: true);
var currentUserId = CurrentUser.GetId();
var currentTenantId = CurrentTenant.Id;
var allActiveTenants = (await TenantStore.GetListAsync()).Where(x => x.IsActive);
var allIdentityLinkUserDtos = identityLinkUsers
.SelectMany(x => new[]
{
new LinkUserDto
{
LinkTenantId = x.TargetTenantId,
LinkUserId = x.TargetUserId,
DirectlyLinked = (x.SourceTenantId == currentTenantId && x.SourceUserId == currentUserId)
|| (x.TargetTenantId == currentTenantId && x.TargetUserId == currentUserId)
},
new LinkUserDto
{
LinkTenantId = x.SourceTenantId,
LinkUserId = x.SourceUserId,
DirectlyLinked = (x.SourceTenantId == currentTenantId && x.SourceUserId == currentUserId)
|| (x.TargetTenantId == currentTenantId && x.TargetUserId == currentUserId)
}
})
.Where(x => x.LinkTenantId != currentTenantId || x.LinkUserId != currentUserId)
.GroupBy(x => new { x.LinkTenantId, x.LinkUserId })
.Select(g => g.FirstOrDefault(x => x.DirectlyLinked) ?? g.First())
.ToList();
foreach (var identityLinkUserDto in allIdentityLinkUserDtos)
{
var linkTenant = allActiveTenants.FirstOrDefault(x => x.Id == identityLinkUserDto.LinkTenantId);
using (CurrentTenant.Change(linkTenant?.Id, linkTenant?.Name))
{
var linkUser = await UserManager.FindByIdAsync(identityLinkUserDto.LinkUserId.ToString());
if (linkUser != null)
{
identityLinkUserDto.LinkUserName = linkUser.UserName;
identityLinkUserDtos.Add(
new LinkUserDto
{
LinkTenantId = linkTenant?.Id,
LinkTenantName = linkTenant?.Name,
LinkUserId = linkUser.Id,
LinkUserName = linkUser.UserName,
DirectlyLinked = identityLinkUserDto.DirectlyLinked,
});
}
}
}
return new ListResultDto<LinkUserDto>(identityLinkUserDtos);
}
public async virtual Task LinkAsync(LinkUserInput input)
{
var identityLinkUserInfo = new IdentityLinkUserInfo(input.UserId, input.TenantId);
if (await IdentityLinkUserManager.VerifyLinkTokenAsync(identityLinkUserInfo, input.Token, LinkUserTokenProviderConsts.LinkUserTokenPurpose))
{
await IdentityLinkUserManager.LinkAsync(
new IdentityLinkUserInfo(CurrentUser.GetId(), CurrentTenant.Id),
identityLinkUserInfo);
return;
}
throw new BusinessException(
LINGYUN.Abp.Identity.IdentityErrorCodes.LinkUserTokenInValid,
"Link user token is invalid!");
}
public async virtual Task UnlinkAsync(UnLinkUserInput input)
{
await IdentityLinkUserManager.UnlinkAsync(
new IdentityLinkUserInfo(CurrentUser.GetId(), CurrentTenant.Id),
new IdentityLinkUserInfo(input.UserId, input.TenantId));
}
public async virtual Task<VerifyLinkUserDto> VerifyLinkUserAsync(VerifyLinkUserInput input)
{
if (await IdentityLinkUserManager.IsLinkedAsync(
new IdentityLinkUserInfo(CurrentUser.GetId(), CurrentTenant.Id),
new IdentityLinkUserInfo(input.UserId, input.TenantId),
includeIndirect: true))
{
using (CurrentTenant.Change(input.TenantId))
{
TenantConfiguration tenant = null;
if (input.TenantId.HasValue)
{
tenant = await TenantStore.FindAsync(input.TenantId.Value);
}
var user = await UserManager.FindByIdAsync(input.UserId.ToString());
return new VerifyLinkUserDto
{
LinkTenantId = tenant?.Id,
LinkTenantName = tenant?.Name,
LinkUserId = user?.Id,
LinkUserName = user?.UserName,
IsLinked = user != null
};
}
}
return new VerifyLinkUserDto { IsLinked = false };
}
[AllowAnonymous]
public async virtual Task<bool> VerifyLinkLoginTokenAsync(VerifyLinkTokenInput input)
{
return await IdentityLinkUserManager.VerifyLinkTokenAsync(
new IdentityLinkUserInfo(input.UserId, input.TenantId),
input.Token,
LinkUserTokenProviderConsts.LinkUserLoginTokenPurpose);
}
[AllowAnonymous]
public async virtual Task<bool> VerifyLinkTokenAsync(VerifyLinkTokenInput input)
{
return await IdentityLinkUserManager.VerifyLinkTokenAsync(
new IdentityLinkUserInfo(input.UserId, input.TenantId),
input.Token,
LinkUserTokenProviderConsts.LinkUserTokenPurpose);
}
}

140
aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN/Abp/Account/AccountController.cs

@ -1,77 +1,77 @@
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Account;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Account;
using Volo.Abp.Application.Dtos;
using Volo.Abp.AspNetCore.Mvc;
namespace LINGYUN.Abp.Account;
[RemoteService(Name = AccountRemoteServiceConsts.RemoteServiceName)]
[Area("account")]
[Route("api/account")]
public class AccountController : AbpControllerBase, IAccountAppService
{
protected IAccountAppService AccountAppService { get; }
public AccountController(
IAccountAppService accountAppService)
{
AccountAppService = accountAppService;
}
[HttpPost]
[Route("wechat/register")]
public async virtual Task RegisterAsync(WeChatRegisterDto input)
{
await AccountAppService.RegisterAsync(input);
}
[HttpPost]
[Route("phone/register")]
public async virtual Task RegisterAsync(PhoneRegisterDto input)
{
await AccountAppService.RegisterAsync(input);
}
[HttpPut]
[Route("phone/reset-password")]
public async virtual Task ResetPasswordAsync(PhoneResetPasswordDto input)
{
await AccountAppService.ResetPasswordAsync(input);
}
[HttpPost]
[Route("phone/send-signin-code")]
public async virtual Task SendPhoneSigninCodeAsync(SendPhoneSigninCodeDto input)
{
await AccountAppService.SendPhoneSigninCodeAsync(input);
}
[HttpPost]
[Route("email/send-signin-code")]
using Volo.Abp.AspNetCore.Mvc;
namespace LINGYUN.Abp.Account;
[RemoteService(Name = AccountRemoteServiceConsts.RemoteServiceName)]
[Area("account")]
[Route("api/account")]
public class AccountController : AbpControllerBase, IAccountAppService
{
protected IAccountAppService AccountAppService { get; }
public AccountController(
IAccountAppService accountAppService)
{
AccountAppService = accountAppService;
}
[HttpPost]
[Route("wechat/register")]
public async virtual Task RegisterAsync(WeChatRegisterDto input)
{
await AccountAppService.RegisterAsync(input);
}
[HttpPost]
[Route("phone/register")]
public async virtual Task RegisterAsync(PhoneRegisterDto input)
{
await AccountAppService.RegisterAsync(input);
}
[HttpPut]
[Route("phone/reset-password")]
public async virtual Task ResetPasswordAsync(PhoneResetPasswordDto input)
{
await AccountAppService.ResetPasswordAsync(input);
}
[HttpPost]
[Route("phone/send-signin-code")]
public async virtual Task SendPhoneSigninCodeAsync(SendPhoneSigninCodeDto input)
{
await AccountAppService.SendPhoneSigninCodeAsync(input);
}
[HttpPost]
[Route("email/send-signin-code")]
public async virtual Task SendEmailSigninCodeAsync(SendEmailSigninCodeDto input)
{
await AccountAppService.SendEmailSigninCodeAsync(input);
}
[HttpPost]
[Route("phone/send-register-code")]
public async virtual Task SendPhoneRegisterCodeAsync(SendPhoneRegisterCodeDto input)
{
await AccountAppService.SendPhoneRegisterCodeAsync(input);
}
[HttpPost]
[Route("phone/send-password-reset-code")]
public async virtual Task SendPhoneResetPasswordCodeAsync(SendPhoneResetPasswordCodeDto input)
{
await AccountAppService.SendPhoneResetPasswordCodeAsync(input);
}
[HttpGet]
[Route("two-factor-providers")]
}
[HttpPost]
[Route("phone/send-register-code")]
public async virtual Task SendPhoneRegisterCodeAsync(SendPhoneRegisterCodeDto input)
{
await AccountAppService.SendPhoneRegisterCodeAsync(input);
}
[HttpPost]
[Route("phone/send-password-reset-code")]
public async virtual Task SendPhoneResetPasswordCodeAsync(SendPhoneResetPasswordCodeDto input)
{
await AccountAppService.SendPhoneResetPasswordCodeAsync(input);
}
[HttpGet]
[Route("two-factor-providers")]
public async virtual Task<ListResultDto<NameValue>> GetTwoFactorProvidersAsync(GetTwoFactorProvidersInput input)
{
return await AccountAppService.GetTwoFactorProvidersAsync(input);
}
}
}
}

70
aspnet-core/modules/account/LINGYUN.Abp.Account.HttpApi/LINGYUN/Abp/Account/IdentityLinkUserController.cs

@ -0,0 +1,70 @@
using LINGYUN.Abp.Account.Dto;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Account;
using Volo.Abp.Application.Dtos;
using Volo.Abp.AspNetCore.Mvc;
namespace LINGYUN.Abp.Account;
[RemoteService(Name = AccountRemoteServiceConsts.RemoteServiceName)]
[Area("account")]
[Route("api/account/link-users")]
public class IdentityLinkUserController(IIdentityLinkUserAppService _service) : AbpControllerBase, IIdentityLinkUserAppService
{
[HttpPost]
[Route("generate-link-login-token")]
public virtual Task<string> GenerateLinkLoginTokenAsync()
{
return _service.GenerateLinkLoginTokenAsync();
}
[HttpPost]
[Route("generate-link-token")]
public virtual Task<string> GenerateLinkTokenAsync()
{
return _service.GenerateLinkTokenAsync();
}
[HttpGet]
public virtual Task<ListResultDto<LinkUserDto>> GetListAsync()
{
return _service.GetListAsync();
}
[HttpPost]
[Route("link")]
public virtual Task LinkAsync(LinkUserInput input)
{
return _service.LinkAsync(input);
}
[HttpPost]
[Route("unlink")]
public virtual Task UnlinkAsync(UnLinkUserInput input)
{
return _service.UnlinkAsync(input);
}
[HttpPost]
[Route("verify-link-user")]
public virtual Task<VerifyLinkUserDto> VerifyLinkUserAsync(VerifyLinkUserInput input)
{
return _service.VerifyLinkUserAsync(input);
}
[HttpPost]
[Route("verify-link-login-token")]
public virtual Task<bool> VerifyLinkLoginTokenAsync(VerifyLinkTokenInput input)
{
return _service.VerifyLinkLoginTokenAsync(input);
}
[HttpPost]
[Route("verify-link-token")]
public virtual Task<bool> VerifyLinkTokenAsync(VerifyLinkTokenInput input)
{
return _service.VerifyLinkTokenAsync(input);
}
}

20
aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Controllers/AuthorizeController.cs

@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using System;
@ -46,9 +47,26 @@ public class AuthorizeController : Volo.Abp.OpenIddict.Controllers.AuthorizeCont
//
// For scenarios where the default authentication handler configured in the ASP.NET Core
// authentication options shouldn't be used, a specific scheme can be specified here.
if (request.HasPromptValue(OpenIddictConstants.PromptValues.Login))
{
var prompt = string.Join(" ", request.GetPromptValues().Remove(OpenIddictConstants.PromptValues.Login));
var parameters = Request.HasFormContentType ?
Request.Form.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList() :
Request.Query.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList();
parameters.Add(KeyValuePair.Create(OpenIddictConstants.Parameters.Prompt, new StringValues(prompt)));
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties()
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
});
}
var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
if (result is not { Succeeded: true } ||
((request.HasPromptValue(OpenIddictConstants.PromptValues.Login) || request.MaxAge is 0 ||
((request.MaxAge is 0 ||
(request.MaxAge is not null && result.Properties?.IssuedUtc is not null &&
TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) &&
TempData["IgnoreAuthenticationChallenge"] is null or false))

2
aspnet-core/modules/account/LINGYUN.Abp.Account.Web.OpenIddict/Pages/Account/SelectAccount.cshtml.cs

@ -233,7 +233,7 @@ public class SelectAccountModel : AccountPageModel
if (RedirectUri.StartsWith("/"))
{
int queryIndex = RedirectUri.IndexOf('?');
var queryIndex = RedirectUri.IndexOf('?');
if (queryIndex >= 0)
{
queryString = RedirectUri.Substring(queryIndex + 1);

26
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/LinkLogged.cshtml

@ -0,0 +1,26 @@
@page
@using Microsoft.AspNetCore.Mvc.Localization;
@using Volo.Abp.Account.Localization;
@using Volo.Abp.MultiTenancy;
@using Volo.Abp.Users;
@model LINGYUN.Abp.Account.Web.Pages.Account.LinkLoggedModel
@inject Volo.Abp.AspNetCore.Mvc.UI.Layout.IPageLayout PageLayout
@inject IHtmlLocalizer<AccountResource> L
@inject ICurrentUser CurrentUser
@inject ICurrentTenant CurrentTenant
@{
PageLayout.Content.Title = L["LinkLogged"].Value;
}
<div>
<div class="gap-2 d-grid">
<a href="@Url.Content(await Model.GetReturnUrlAsync(Model.ReturnUrl, Model.ReturnUrlHash))" class="mb-3 btn btn-primary"> @L["StayWithCurrentAccount"]</a>
</div>
<form method="post" asp-page="/Account/LinkLogged" id="linkUserLoginForm" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash">
<input asp-for="LinkTenantId">
<input asp-for="LinkUserId">
<div class="gap-2 d-grid">
<abp-button class="btn-secondary">@L["LogInUsingYourProviderAccount", Model.LinkTenantAndUserName] &rarr;</abp-button>
</div>
</form>
</div>

106
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/LinkLogged.cshtml.cs

@ -0,0 +1,106 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp.Identity;
using Volo.Abp.Security.Claims;
namespace LINGYUN.Abp.Account.Web.Pages.Account;
[Authorize]
public class LinkLoggedModel : AccountPageModel
{
[BindProperty(SupportsGet = true)]
public string ReturnUrl { get; set; }
[BindProperty(SupportsGet = true)]
public string ReturnUrlHash { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid LinkUserId { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid? LinkTenantId { get; set; }
public string LinkTenantAndUserName { get; set; }
protected ICurrentPrincipalAccessor CurrentPrincipalAccessor => LazyServiceProvider.LazyGetRequiredService<ICurrentPrincipalAccessor>();
public IIdentityLinkUserAppService IdentityLinkUserAppService => LazyServiceProvider.LazyGetRequiredService<IIdentityLinkUserAppService>();
public IdentityDynamicClaimsPrincipalContributorCache IdentityDynamicClaimsPrincipalContributorCache => LazyServiceProvider.LazyGetRequiredService<IdentityDynamicClaimsPrincipalContributorCache>();
public virtual async Task<IActionResult> OnGetAsync()
{
var validLinkUser = await IdentityLinkUserAppService.VerifyLinkUserAsync(
new Dto.VerifyLinkUserInput
{
UserId = LinkUserId,
TenantId = LinkTenantId,
});
if (!validLinkUser.IsLinked)
{
return await RedirectToLoginPageAsync();
}
LinkTenantAndUserName = !validLinkUser.LinkTenantName.IsNullOrWhiteSpace()
? $"{validLinkUser.LinkTenantName}\\{validLinkUser.LinkUserName}"
: validLinkUser.LinkUserName;
return Page();
}
public async virtual Task<IActionResult> OnPostAsync()
{
if (LinkUserId == CurrentUser.Id && LinkTenantId == CurrentTenant.Id)
{
return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash);
}
using (CurrentTenant.Change(LinkTenantId))
{
var sourceUser = await UserManager.GetByIdAsync(LinkUserId);
using (CurrentPrincipalAccessor.Change(await SignInManager.CreateUserPrincipalAsync(sourceUser)))
{
var validLinkUser = await IdentityLinkUserAppService.VerifyLinkUserAsync(
new Dto.VerifyLinkUserInput
{
UserId = LinkUserId,
TenantId = LinkTenantId,
});
if (validLinkUser.IsLinked)
{
var isPersistent = (await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme))?.Properties?.IsPersistent ?? false;
await SignInManager.SignOutAsync();
using (CurrentTenant.Change(LinkTenantId))
{
var targetUser = await UserManager.GetByIdAsync(LinkUserId);
await SignInManager.SignInAsync(targetUser, isPersistent);
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(targetUser.Id, targetUser.TenantId);
}
return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash);
}
}
}
Alerts.Warning(L["Volo.Abp.Identity:InvalidToken"]);
return Page();
}
public virtual Task<string> GetReturnUrlAsync(string returnUrl, string returnUrlHash)
{
return base.GetRedirectUrlAsync(returnUrl, returnUrlHash);
}
protected virtual Task<IActionResult> RedirectToLoginPageAsync()
{
return Task.FromResult<IActionResult>(RedirectToPage("./Login", new
{
ReturnUrl,
ReturnUrlHash
}));
}
}

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

@ -24,13 +24,23 @@
}
<div class="account-module-form">
@if (Model.IsLinkLogin)
{
<abp-alert alert-type="Warning">
@L["LinkAccountWarning", await Model.GetWithoutLinkReturnUrlAsync(Model.ReturnUrl, Model.ReturnUrlHash)]
</abp-alert>
}
@if (Model.EnableLocalLogin)
{
<abp-tabs id="LoginFormTabs">
<abp-tab title="@L["PasswordLogin"].Value" active="Model.LoginType == LoginType.Password" header-name="PasswordLogin">
<form method="post" class="mt-4" asp-page-handler="PasswordLogin">
<input asp-for="PasswordLoginInput.ReturnUrl" />
<input asp-for="PasswordLoginInput.ReturnUrlHash" />
<input asp-for="ReturnUrl" />
<input asp-for="ReturnUrlHash" />
<input asp-for="LinkTenantId" />
<input asp-for="LinkUserId" />
<input asp-for="LinkToken" />
<div class="mb-3">
<label asp-for="PasswordLoginInput.UserNameOrEmailAddress" class="form-label"></label>
<input asp-for="PasswordLoginInput.UserNameOrEmailAddress" class="form-control" />
@ -79,8 +89,11 @@
</abp-tab>
<abp-tab title="@L["PhoneNumberLogin"].Value" active="Model.LoginType == LoginType.PhoneNumber" header-name="PhoneNumberLogin">
<form method="post" class="mt-4" asp-page-handler="PhoneNumberLogin" id="PhoneNumberForm">
<input asp-for="PhoneLoginInput.ReturnUrl" />
<input asp-for="PhoneLoginInput.ReturnUrlHash" />
<input asp-for="ReturnUrl" />
<input asp-for="ReturnUrlHash" />
<input asp-for="LinkTenantId" />
<input asp-for="LinkUserId" />
<input asp-for="LinkToken" />
<div class="mb-3">
<label asp-for="PhoneLoginInput.PhoneNumber" class="form-label"></label>
<input asp-for="PhoneLoginInput.PhoneNumber" class="form-control" id="PhoneNumberInput" />
@ -121,8 +134,11 @@
{
<abp-tab title="@L["ScanQrCodeLogin"].Value" active="Model.LoginType == LoginType.QrCode" header-name="QrCodeLogin">
<form method="post" class="mt-4" asp-page-handler="QrCodeLogin" id="QrCodeForm">
<input asp-for="QrCodeLoginInput.ReturnUrl" />
<input asp-for="QrCodeLoginInput.ReturnUrlHash" />
<input asp-for="ReturnUrl" />
<input asp-for="ReturnUrlHash" />
<input asp-for="LinkTenantId" />
<input asp-for="LinkUserId" />
<input asp-for="LinkToken" />
<input asp-for="QrCodeLoginInput.Key" id="QrCodeKey" />
<div class="gap-2" style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
<div id="QrCode"></div>

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

@ -1,11 +1,15 @@
using LINGYUN.Abp.Account.Dto;
using LINGYUN.Abp.Account.Web.ExternalProviders;
using LINGYUN.Abp.Account.Web.Models;
using LINGYUN.Abp.Identity.QrCode;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MyCSharp.HttpUserAgentParser.Providers;
@ -15,6 +19,7 @@ using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Account.Settings;
@ -66,8 +71,29 @@ public class LoginModel : AccountPageModel
protected IIdentityUserRepository UserRepository => LazyServiceProvider.LazyGetRequiredService<IIdentityUserRepository>();
protected IQrCodeLoginProvider QrCodeLoginProvider => LazyServiceProvider.LazyGetRequiredService<IQrCodeLoginProvider>();
protected ICurrentPrincipalAccessor CurrentPrincipalAccessor => LazyServiceProvider.LazyGetRequiredService<ICurrentPrincipalAccessor>();
protected IHttpUserAgentParserProvider HttpUserAgentParserProvider => LazyServiceProvider.LazyGetRequiredService<IHttpUserAgentParserProvider>();
#region LinkUser
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid? LinkUserId { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid? LinkTenantId { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public string LinkToken { get; set; }
public bool IsLinkLogin { get; set; }
public IIdentityLinkUserAppService IdentityLinkUserAppService => LazyServiceProvider.LazyGetRequiredService<IIdentityLinkUserAppService>();
#endregion
protected IExternalProviderService ExternalProviderService { get; }
protected IAuthenticationSchemeProvider SchemeProvider { get; }
protected AbpAccountOptions AccountOptions { get; }
@ -89,21 +115,9 @@ public class LoginModel : AccountPageModel
public virtual async Task<IActionResult> OnGetAsync()
{
LoginType = LoginType.Password;
PhoneLoginInput = new PhoneLoginInputModel
{
ReturnUrl = ReturnUrl,
ReturnUrlHash = ReturnUrlHash,
};
QrCodeLoginInput = new QrCodeLoginInputModel
{
ReturnUrl = ReturnUrl,
ReturnUrlHash = ReturnUrlHash,
};
PasswordLoginInput = new PasswordLoginInputModel
{
ReturnUrl = ReturnUrl,
ReturnUrlHash = ReturnUrlHash,
};
PhoneLoginInput = new PhoneLoginInputModel();
QrCodeLoginInput = new QrCodeLoginInputModel();
PasswordLoginInput = new PasswordLoginInputModel();
AllowQrCodeLoginIfNotMobileDevice();
@ -116,6 +130,20 @@ public class LoginModel : AccountPageModel
return await OnPostExternalLogin(ExternalProviders.First().AuthenticationScheme);
}
IsLinkLogin = await VerifyLinkTokenAsync();
if (IsLinkLogin && CurrentUser.IsAuthenticated)
{
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
Action = IdentitySecurityLogActionConsts.Logout
});
await SignInManager.SignOutAsync();
return Redirect(HttpContext.Request.GetDisplayUrl());
}
return Page();
}
@ -140,6 +168,8 @@ public class LoginModel : AccountPageModel
await IdentityOptions.SetAsync();
IsLinkLogin = await VerifyLinkTokenAsync();
var result = await SignInManager.PasswordSignInAsync(
PasswordLoginInput.UserNameOrEmailAddress,
PasswordLoginInput.Password,
@ -179,10 +209,15 @@ public class LoginModel : AccountPageModel
Debug.Assert(user != null, nameof(user) + " != null");
if (IsLinkLogin)
{
return await HandleLinkUserLogin(user);
}
// Clear the dynamic claims cache.
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId);
return await RedirectSafelyAsync(PasswordLoginInput.ReturnUrl, PasswordLoginInput.ReturnUrlHash);
return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash);
}
public async virtual Task<IActionResult> OnPostPhoneNumberLogin(string action)
@ -217,6 +252,11 @@ public class LoginModel : AccountPageModel
return Page();
}
if (IsLinkLogin)
{
return await HandleLinkUserLogin(user);
}
await SignInManager.SignInAsync(user, PhoneLoginInput.RememberMe);
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
@ -229,7 +269,7 @@ public class LoginModel : AccountPageModel
// Clear the dynamic claims cache.
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId);
return await RedirectSafelyAsync(PhoneLoginInput.ReturnUrl, PhoneLoginInput.ReturnUrlHash);
return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash);
}
protected virtual void SetTenantCookies(Guid? tenantId = null)
@ -296,6 +336,11 @@ public class LoginModel : AccountPageModel
return Page();
}
if (IsLinkLogin)
{
return await HandleLinkUserLogin(user);
}
// TODO: 션遼되쩌
await SignInManager.SignInAsync(user, true);
@ -311,7 +356,7 @@ public class LoginModel : AccountPageModel
// Clear the dynamic claims cache.
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId);
return await RedirectSafelyAsync(QrCodeLoginInput.ReturnUrl, QrCodeLoginInput.ReturnUrlHash);
return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash);
}
}
@ -398,7 +443,10 @@ public class LoginModel : AccountPageModel
{
IsExternalLogin = true,
ExternalLoginAuthSchema = loginInfo.LoginProvider,
ReturnUrl = returnUrl
ReturnUrl = returnUrl,
LinkUserId,
LinkTenantId,
LinkToken
});
}
@ -409,7 +457,10 @@ public class LoginModel : AccountPageModel
{
IsExternalLogin = true,
ExternalLoginAuthSchema = loginInfo.LoginProvider,
ReturnUrl = returnUrl
ReturnUrl = returnUrl,
LinkUserId,
LinkTenantId,
LinkToken
});
}
@ -438,9 +489,12 @@ public class LoginModel : AccountPageModel
// 路땍蕨崗凜羹훰聯女충
return Task.FromResult<IActionResult>(RedirectToPage("SendCode", new
{
returnUrl = PasswordLoginInput.ReturnUrl,
returnUrlHash = PasswordLoginInput.ReturnUrlHash,
rememberMe = PasswordLoginInput.RememberMe
returnUrl = ReturnUrl,
returnUrlHash = ReturnUrlHash,
rememberMe = PasswordLoginInput.RememberMe,
linkUserId = LinkUserId,
linkTenantId = LinkTenantId,
linkToken = LinkToken,
}));
}
@ -535,9 +589,9 @@ public class LoginModel : AccountPageModel
return RedirectToPage("ChangePassword", new
{
returnUrl = PasswordLoginInput.ReturnUrl,
returnUrlHash = PasswordLoginInput.ReturnUrlHash,
rememberMe = PasswordLoginInput.RememberMe
returnUrl = ReturnUrl,
returnUrlHash = ReturnUrlHash,
rememberMe = PasswordLoginInput.RememberMe,
});
}
}
@ -562,7 +616,7 @@ public class LoginModel : AccountPageModel
return RedirectToPage("./ChangePassword", new
{
ReturnUrl,
ReturnUrlHash
ReturnUrlHash,
});
}
@ -612,17 +666,118 @@ public class LoginModel : AccountPageModel
}
}
}
#region LinkUser
public async virtual Task<string> GetWithoutLinkReturnUrlAsync(string returnUrl, string returnUrlHash)
{
var redirectUrl = await base.GetRedirectUrlAsync(returnUrl, returnUrlHash);
// 使用正则表达式移除 LinkUser 参数
redirectUrl = Regex.Replace(redirectUrl, @"[&?]LinkToken=[^&]*", "");
redirectUrl = Regex.Replace(redirectUrl, @"[&?]LinkUserId=[^&]*", "");
redirectUrl = Regex.Replace(redirectUrl, @"[&?]LinkTenantId=[^&]*", "");
redirectUrl = Regex.Replace(redirectUrl, @"[&?]linkUserId=[^&]*", "");
redirectUrl = Regex.Replace(redirectUrl, @"[&?]linkToken=[^&]*", "");
return redirectUrl;
}
protected async virtual Task<bool> VerifyLinkTokenAsync()
{
if (LinkToken.IsNullOrWhiteSpace() || !LinkUserId.HasValue)
{
if (string.IsNullOrWhiteSpace(ReturnUrl))
{
return false;
}
var queryString = QueryHelpers.ParseQuery(ReturnUrl);
queryString.TryGetValue("LinkToken", out var linkTokenVal);
queryString.TryGetValue("LinkUserId", out var linkUserIdVal);
queryString.TryGetValue("LinkTenantId", out var linkTenantIdVal);
if (!linkTokenVal.IsNullOrEmpty() && !linkUserIdVal.IsNullOrEmpty() &&
Guid.TryParse(linkUserIdVal.ToString(), out var linkUserId))
{
LinkToken = linkTokenVal.ToString();
LinkUserId = linkUserId;
if (Guid.TryParse(linkTenantIdVal.ToString(), out var linkTenantId))
{
LinkTenantId = linkTenantId;
}
}
else
{
return false;
}
}
// TODO: 替换框架对于空格符号的编码错误
LinkToken = LinkToken.Replace(" ", "+");
return await IdentityLinkUserAppService.VerifyLinkTokenAsync(new VerifyLinkTokenInput()
{
UserId = LinkUserId.Value,
TenantId = LinkTenantId,
Token = LinkToken
});
}
protected async virtual Task<IActionResult> HandleLinkUserLogin(IdentityUser user)
{
using (CurrentPrincipalAccessor.Change(await SignInManager.CreateUserPrincipalAsync(user)))
{
await IdentityLinkUserAppService.LinkAsync(new LinkUserInput
{
UserId = LinkUserId.Value,
TenantId = LinkTenantId,
Token = LinkToken
});
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
UserName = user.UserName,
Action = "LinkUser",
ExtraProperties =
{
{ "LinkTenantId", LinkTenantId },
{ "LinkUserId", LinkUserId }
}
});
using (CurrentTenant.Change(LinkTenantId))
{
user = await UserManager.GetByIdAsync(LinkUserId.Value);
using (CurrentPrincipalAccessor.Change(await SignInManager.CreateUserPrincipalAsync(user)))
{
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
UserName = user.UserName,
Action = "LinkUser",
ExtraProperties =
{
{ "LinkTenantId", LinkTenantId },
{ "LinkUserId", LinkUserId }
}
});
}
}
return RedirectToPage("./LinkLogged", new
{
ReturnUrl,
ReturnUrlHash,
LinkUserId,
LinkTenantId
});
}
}
#endregion
}
public abstract class LoginInputModel
{
[HiddenInput]
[BindProperty(SupportsGet = true)]
public string ReturnUrl { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public string ReturnUrlHash { get; set; }
}
public class PhoneLoginInputModel : LoginInputModel

97
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Register.cshtml.cs

@ -1,3 +1,4 @@
using LINGYUN.Abp.Account.Dto;
using LINGYUN.Abp.Account.Web.ExternalProviders;
using LINGYUN.Abp.Account.Web.Models;
using Microsoft.AspNetCore.Authentication;
@ -46,6 +47,25 @@ public class RegisterModel : AccountPageModel
[BindProperty(SupportsGet = true)]
public string ExternalLoginAuthSchema { get; set; }
#region LinkUser
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid? LinkUserId { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid? LinkTenantId { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public string LinkToken { get; set; }
protected ICurrentPrincipalAccessor CurrentPrincipalAccessor => LazyServiceProvider.LazyGetRequiredService<ICurrentPrincipalAccessor>();
public IIdentityLinkUserAppService IdentityLinkUserAppService => LazyServiceProvider.LazyGetRequiredService<IIdentityLinkUserAppService>();
#endregion
public IEnumerable<ExternalLoginProviderModel> ExternalProviders { get; set; }
public IEnumerable<ExternalLoginProviderModel> VisibleExternalProviders => ExternalProviders.Where(x => !string.IsNullOrWhiteSpace(x.DisplayName));
public bool EnableLocalRegister { get; set; }
@ -146,7 +166,17 @@ public class RegisterModel : AccountPageModel
}
else
{
await RegisterLocalUserAsync();
var user = await RegisterLocalUserAsync();
if (await VerifyLinkTokenAsync())
{
await HandleLinkUserLogin(user);
}
await SignInManager.SignInAsync(user, isPersistent: true);
// Clear the dynamic claims cache.
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId);
}
return Redirect(ReturnUrl ?? "~/"); //TODO: How to ensure safety? IdentityServer requires it however it should be checked somehow!
@ -158,7 +188,7 @@ public class RegisterModel : AccountPageModel
}
}
protected virtual async Task RegisterLocalUserAsync()
protected virtual async Task<IdentityUser> RegisterLocalUserAsync()
{
ValidateModel();
@ -172,11 +202,7 @@ public class RegisterModel : AccountPageModel
}
);
var user = await UserManager.GetByIdAsync(userDto.Id);
await SignInManager.SignInAsync(user, isPersistent: true);
// Clear the dynamic claims cache.
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId);
return await UserManager.GetByIdAsync(userDto.Id);
}
protected virtual async Task RegisterExternalUserAsync(ExternalLoginInfo externalLoginInfo, string userName, string emailAddress)
@ -271,6 +297,63 @@ public class RegisterModel : AccountPageModel
return await Task.FromResult(Challenge(properties, provider));
}
#region LinkUser
protected async virtual Task HandleLinkUserLogin(IdentityUser user)
{
using (CurrentPrincipalAccessor.Change(await SignInManager.CreateUserPrincipalAsync(user)))
{
await IdentityLinkUserAppService.LinkAsync(new LinkUserInput
{
UserId = LinkUserId.Value,
TenantId = LinkTenantId,
Token = LinkToken
});
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
UserName = user.UserName,
Action = "LinkUser",
ExtraProperties =
{
{ "LinkTenantId", LinkTenantId },
{ "LinkUserId", LinkUserId }
}
});
using (CurrentTenant.Change(LinkTenantId))
{
var targetUser = await UserManager.GetByIdAsync(LinkUserId.Value);
using (CurrentPrincipalAccessor.Change(await SignInManager.CreateUserPrincipalAsync(targetUser)))
{
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
UserName = targetUser.UserName,
Action = "LinkUser",
ExtraProperties =
{
{ "LinkTenantId", LinkTenantId },
{ "LinkUserId", LinkUserId }
}
});
}
}
}
}
protected virtual async Task<bool> VerifyLinkTokenAsync()
{
return !LinkToken.IsNullOrWhiteSpace() && LinkUserId != null
&& await IdentityLinkUserAppService.VerifyLinkTokenAsync(new VerifyLinkTokenInput
{
UserId = LinkUserId.Value,
TenantId = LinkTenantId,
Token = LinkToken
});
}
#endregion
public class PostInput
{
[Required]

20
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/SendCode.cshtml.cs

@ -2,6 +2,7 @@ using LINGYUN.Abp.Account.Security;
using LINGYUN.Abp.Identity.Settings;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -29,6 +30,18 @@ namespace LINGYUN.Abp.Account.Web.Pages.Account
[BindProperty(SupportsGet = true)]
public bool RememberMe { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid? LinkUserId { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid? LinkTenantId { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public string LinkToken { get; set; }
public IEnumerable<SelectListItem> Providers { get; set; }
protected ISmsSender SmsSender { get; }
@ -78,7 +91,7 @@ namespace LINGYUN.Abp.Account.Web.Pages.Account
{
returnUrl = ReturnUrl,
returnUrlHash = ReturnUrlHash,
rememberMe = RememberMe
rememberMe = RememberMe,
});
}
// Éú³ÉÑéÖ¤Âë
@ -116,7 +129,10 @@ namespace LINGYUN.Abp.Account.Web.Pages.Account
provider = Input.SelectedProvider,
returnUrl = ReturnUrl,
returnUrlHash = ReturnUrlHash,
rememberMe = RememberMe
rememberMe = RememberMe,
linkUserId = LinkUserId,
linkTenantId = LinkTenantId,
linkToken = LinkToken
});
}
}

83
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/VerifyCode.cshtml.cs

@ -1,10 +1,13 @@
using LINGYUN.Abp.Account.Dto;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Volo.Abp.Account.Localization;
using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp.Identity;
using Volo.Abp.Security.Claims;
namespace LINGYUN.Abp.Account.Web.Pages.Account
{
@ -39,6 +42,25 @@ namespace LINGYUN.Abp.Account.Web.Pages.Account
[BindProperty(SupportsGet = true)]
public bool RememberMe { get; set; }
#region LinkUser
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid? LinkUserId { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid? LinkTenantId { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public string LinkToken { get; set; }
protected ICurrentPrincipalAccessor CurrentPrincipalAccessor => LazyServiceProvider.LazyGetRequiredService<ICurrentPrincipalAccessor>();
public IIdentityLinkUserAppService IdentityLinkUserAppService => LazyServiceProvider.LazyGetRequiredService<IIdentityLinkUserAppService>();
#endregion
public VerifyCodeModel(
IdentityDynamicClaimsPrincipalContributorCache identityDynamicClaimsPrincipalContributorCache)
{
@ -67,6 +89,10 @@ namespace LINGYUN.Abp.Account.Web.Pages.Account
var result = await SignInManager.TwoFactorSignInAsync(Provider, Input.VerifyCode, RememberMe, Input.RememberBrowser);
if (result.Succeeded)
{
if (await VerifyLinkTokenAsync())
{
await HandleLinkUserLogin(user);
}
// Clear the dynamic claims cache.
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId);
@ -84,6 +110,63 @@ namespace LINGYUN.Abp.Account.Web.Pages.Account
return Page();
}
}
#region LinkUser
protected async virtual Task HandleLinkUserLogin(IdentityUser user)
{
using (CurrentPrincipalAccessor.Change(await SignInManager.CreateUserPrincipalAsync(user)))
{
await IdentityLinkUserAppService.LinkAsync(new LinkUserInput
{
UserId = LinkUserId.Value,
TenantId = LinkTenantId,
Token = LinkToken
});
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
UserName = user.UserName,
Action = "LinkUser",
ExtraProperties =
{
{ "LinkTenantId", LinkTenantId },
{ "LinkUserId", LinkUserId }
}
});
using (CurrentTenant.Change(LinkTenantId))
{
var targetUser = await UserManager.GetByIdAsync(LinkUserId.Value);
using (CurrentPrincipalAccessor.Change(await SignInManager.CreateUserPrincipalAsync(targetUser)))
{
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
UserName = targetUser.UserName,
Action = "LinkUser",
ExtraProperties =
{
{ "LinkTenantId", LinkTenantId },
{ "LinkUserId", LinkUserId }
}
});
}
}
}
}
protected virtual async Task<bool> VerifyLinkTokenAsync()
{
return !LinkToken.IsNullOrWhiteSpace() && LinkUserId != null
&& await IdentityLinkUserAppService.VerifyLinkTokenAsync(new VerifyLinkTokenInput
{
UserId = LinkUserId.Value,
TenantId = LinkTenantId,
Token = LinkToken
});
}
#endregion
}
public class VerifyCodeInputModel

4
aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/IdentityErrorCodes.cs

@ -38,4 +38,8 @@ public class IdentityErrorCodes
/// 密码不能与最近{0}次使用的密码相同
/// </summary>
public const string PasswordInHistoryInValid = "Volo.Abp.Identity:020013";
/// <summary>
/// 关联用户Token无效
/// </summary>
public const string LinkUserTokenInValid = "Volo.Abp.Identity:020014";
}

1
aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/en.json

@ -54,6 +54,7 @@
"Volo.Abp.Identity:020011": "Secondary authentication cannot be enabled when an MFA device is not attached!",
"Volo.Abp.Identity:020012": "Invalid authenticator code!",
"Volo.Abp.Identity:020013": "The password cannot be the same as the previous one.!",
"Volo.Abp.Identity:020014": "Link user token is invalid!",
"Volo.Abp.Identity:DuplicatePhoneNumber": "Phone number '{0}' is already taken.",
"DisplayName:Abp.Identity.User.SmsNewUserRegister": "Register sms template",
"Description:Abp.Identity.User.SmsNewUserRegister": "When the user registers, he/she should send the template number of the SMS verification code and fill in the template number of the corresponding cloud platform registration",

1
aspnet-core/modules/identity/LINGYUN.Abp.Identity.Domain.Shared/LINGYUN/Abp/Identity/Localization/zh-Hans.json

@ -54,6 +54,7 @@
"Volo.Abp.Identity:020011": "未绑定MFA设备时无法启用二次认证!",
"Volo.Abp.Identity:020012": "无效的验证器代码!",
"Volo.Abp.Identity:020013": "密码不能与之前的密码相同!",
"Volo.Abp.Identity:020014": "关联用户Token无效!",
"Volo.Abp.Identity:DuplicatePhoneNumber": "手机号 '{0}' 已存在.",
"DisplayName:Abp.Identity.User.SmsNewUserRegister": "新用户注册模板",
"Description:Abp.Identity.User.SmsNewUserRegister": "新用户通过手机注册账号验证码模板",

1
aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.LinkUser/LINGYUN/Abp/OpenIddict/LinkUser/AbpOpenIddictLinkUserModule.cs

@ -17,6 +17,7 @@ public class AbpOpenIddictLinkUserModule : AbpModule
PreConfigure<OpenIddictServerBuilder>(builder =>
{
builder.AllowLinkUserFlow();
builder.AddEventHandler(LinkUserAuthorizationHandler.Descriptor);
});
}

37
aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.LinkUser/LINGYUN/Abp/OpenIddict/LinkUser/LinkUserAuthorizationHandler.cs

@ -0,0 +1,37 @@
using Microsoft.AspNetCore;
using OpenIddict.Server;
using System.Linq;
using System.Threading.Tasks;
using static OpenIddict.Server.OpenIddictServerEvents;
namespace LINGYUN.Abp.OpenIddict.LinkUser;
public class LinkUserAuthorizationHandler : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
{
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.UseScopedHandler<LinkUserAuthorizationHandler>()
.SetOrder(2000)
.SetType(OpenIddictServerHandlerType.Custom)
.Build();
public async ValueTask HandleAsync(ValidateAuthorizationRequestContext context)
{
var request = context.Transaction.GetHttpRequest();
if (request == null)
{
return;
}
var linkUserId = request.Query["LinkUserId"].FirstOrDefault();
var linkToken = request.Query["LinkToken"].FirstOrDefault();
if (string.IsNullOrEmpty(linkUserId) || string.IsNullOrEmpty(linkToken))
{
return;
}
context.Transaction.SetProperty("LinkUserId", linkUserId);
context.Transaction.SetProperty("LinkToken", linkToken);
}
}
Loading…
Cancel
Save