Browse Source

feat(vben5): Add authority server center login

- add `oidc-client-ts` package
- add `onlyOidc` environment variable
- add oidc login and token refresh functions to `useAuthStore`
- If `onlyOidc` is enabled to guide users to jump to the certification authority server
- third-party login only provides certification center server
pull/1220/head
colin 8 months ago
parent
commit
65134dd9ec
  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

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

Loading…
Cancel
Save