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 10 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 # 是否注入全局loading
VITE_INJECT_APP_LOADING=true 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" VITE_GLOB_AUDIENCE="openid email address phone profile offline_access lingyun-abp-application"
# 客户端Id
VITE_GLOB_CLIENT_ID=vue-admin-client VITE_GLOB_CLIENT_ID=vue-admin-client
# 客户端密钥【生产环境请勿设置此值,建议启用仅允许OIDC登录,将使用授权码类型登录】
VITE_GLOB_CLIENT_SECRET=1q2w3e* VITE_GLOB_CLIENT_SECRET=1q2w3e*

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

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

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

@ -5,7 +5,7 @@ import {
} from '@vben/request'; } from '@vben/request';
import { useAccessStore } from '@vben/stores'; import { useAccessStore } from '@vben/stores';
import { useOAuthError, useTokenApi } from '@abp/account'; import { useOAuthError } from '@abp/account';
import { useAbpStore } from '@abp/core'; import { useAbpStore } from '@abp/core';
import { requestClient, useWrapperResult } from '@abp/request'; import { requestClient, useWrapperResult } from '@abp/request';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
@ -13,7 +13,6 @@ import { message } from 'ant-design-vue';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
export function initRequestClient() { export function initRequestClient() {
const { refreshTokenApi } = useTokenApi();
/** /**
* *
*/ */
@ -36,19 +35,11 @@ export function initRequestClient() {
* token逻辑 * token逻辑
*/ */
async function doRefreshToken() { async function doRefreshToken() {
const accessStore = useAccessStore(); const authStore = useAuthStore();
if (accessStore.refreshToken) { try {
try { return await authStore.refreshSession();
const { accessToken, tokenType, refreshToken } = await refreshTokenApi({ } catch {
refreshToken: accessStore.refreshToken, console.warn('The refresh token has expired or is unavailable.');
});
const newToken = `${tokenType} ${accessToken}`;
accessStore.setAccessToken(newToken);
accessStore.setRefreshToken(refreshToken);
return newToken;
} catch {
console.warn('The refresh token has expired or is unavailable.');
}
} }
return ''; 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", "register": "Register",
"codeLogin": "Code Login", "codeLogin": "Code Login",
"qrcodeLogin": "Qr 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": { "dashboard": {
"title": "Dashboard", "title": "Dashboard",

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

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

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

@ -21,6 +21,17 @@ const fallbackNotFoundRoute: RouteRecordRaw = {
/** 基本路由,这些路由是必须存在的 */ /** 基本路由,这些路由是必须存在的 */
const coreRoutes: 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 * 使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 type { Recordable, UserInfo } from '@vben/types';
import { ref, watch } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
@ -20,11 +20,12 @@ import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useAbpConfigApi } from '#/api/core/useAbpConfigApi'; import { useAbpConfigApi } from '#/api/core/useAbpConfigApi';
import authService from '#/auth/authService';
import { $t } from '#/locales'; import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const { publish } = useEventBus(); const { publish } = useEventBus();
const { loginApi } = useTokenApi(); const { loginApi, refreshTokenApi } = useTokenApi();
const { loginApi: qrcodeLoginApi } = useQrCodeLoginApi(); const { loginApi: qrcodeLoginApi } = useQrCodeLoginApi();
const { loginApi: phoneLoginApi } = usePhoneLoginApi(); const { loginApi: phoneLoginApi } = usePhoneLoginApi();
const { getUserInfoApi } = useUserInfoApi(); const { getUserInfoApi } = useUserInfoApi();
@ -37,16 +38,45 @@ export const useAuthStore = defineStore('auth', () => {
const loginLoading = ref(false); const loginLoading = ref(false);
watch( async function refreshSession() {
() => accessStore.accessToken, if (await authService.getAccessToken()) {
(accessToken) => { const user = await authService.refreshToken();
if (accessToken && !loginLoading.value) { const newToken = `${user?.token_type} ${user?.access_token}`;
loginLoading.value = true; accessStore.setAccessToken(newToken);
fetchUserInfo(); if (user?.refresh_token) {
loginLoading.value = false; 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( async function qrcodeLogin(
key: string, key: string,
@ -96,7 +126,10 @@ export const useAuthStore = defineStore('auth', () => {
async function logout(redirect: boolean = true) { async function logout(redirect: boolean = true) {
try { try {
// await logoutApi(); if (await authService.getAccessToken()) {
accessStore.setAccessToken(null);
await authService.logout();
}
} catch { } catch {
// 不做任何处理 // 不做任何处理
} }
@ -200,8 +233,11 @@ export const useAuthStore = defineStore('auth', () => {
authLogin, authLogin,
phoneLogin, phoneLogin,
qrcodeLogin, qrcodeLogin,
oidcLogin,
oidcCallback,
fetchUserInfo, fetchUserInfo,
loginLoading, loginLoading,
logout, 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 { computed, nextTick, onMounted, useTemplateRef } from 'vue';
import { AuthenticationLogin, useVbenModal, z } from '@vben/common-ui'; import { AuthenticationLogin, useVbenModal, z } from '@vben/common-ui';
import { useAppConfig } from '@vben/hooks';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { useAbpStore, useSettings } from '@abp/core'; import { useAbpStore, useSettings } from '@abp/core';
import { Modal } from 'ant-design-vue';
import { useAbpConfigApi } from '#/api/core/useAbpConfigApi'; import { useAbpConfigApi } from '#/api/core/useAbpConfigApi';
import { useAuthStore } from '#/store'; import { useAuthStore } from '#/store';
@ -24,6 +26,8 @@ interface LoginInstance {
defineOptions({ name: 'Login' }); defineOptions({ name: 'Login' });
const { onlyOidc } = useAppConfig(import.meta.env, import.meta.env.PROD);
const abpStore = useAbpStore(); const abpStore = useAbpStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
@ -34,6 +38,9 @@ const { getConfigApi } = useAbpConfigApi();
const login = useTemplateRef<LoginInstance>('login'); const login = useTemplateRef<LoginInstance>('login');
const formSchema = computed((): VbenFormSchema[] => { const formSchema = computed((): VbenFormSchema[] => {
if (onlyOidc) {
return [];
}
let schemas: VbenFormSchema[] = [ let schemas: VbenFormSchema[] = [
{ {
component: 'Input', component: 'Input',
@ -75,6 +82,24 @@ const [ShouldChangePasswordModal, changePasswordModalApi] = useVbenModal({
connectedComponent: ShouldChangePassword, connectedComponent: ShouldChangePassword,
}); });
async function onInit() { 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(); const abpConfig = await getConfigApi();
abpStore.setApplication(abpConfig); abpStore.setApplication(abpConfig);
nextTick(() => { nextTick(() => {
@ -83,6 +108,10 @@ async function onInit() {
}); });
} }
async function onLogin(params: Recordable<any>) { async function onLogin(params: Recordable<any>) {
if (onlyOidc === true) {
await authStore.oidcLogin();
return;
}
try { try {
await authStore.authLogin(params); await authStore.authLogin(params);
} catch (error) { } catch (error) {
@ -115,7 +144,7 @@ onMounted(onInit);
</script> </script>
<template> <template>
<div> <div v-if="!onlyOidc">
<AuthenticationLogin <AuthenticationLogin
ref="login" ref="login"
:form-schema="formSchema" :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"> <script setup lang="ts">
import { MdiGithub, MdiGoogle, MdiQqchat, MdiWechat } from '@vben/icons'; import { Button } from 'ant-design-vue';
import { $t } from '@vben/locales';
import { VbenIconButton } from '@vben-core/shadcn-ui'; import { useAuthStore } from '#/store/auth';
defineOptions({ defineOptions({
name: 'ThirdPartyLogin', name: 'ThirdPartyLogin',
}); });
const authStore = useAuthStore();
async function login() {
await authStore.oidcLogin();
}
</script> </script>
<template> <template>
@ -20,18 +25,9 @@ defineOptions({
</div> </div>
<div class="mt-4 flex flex-wrap justify-center"> <div class="mt-4 flex flex-wrap justify-center">
<VbenIconButton class="mb-3"> <Button block type="primary" ghost @click="login">
<MdiWechat /> {{ $t('page.auth.oidcLogin') }}
</VbenIconButton> </Button>
<VbenIconButton class="mb-3">
<MdiQqchat />
</VbenIconButton>
<VbenIconButton class="mb-3">
<MdiGithub />
</VbenIconButton>
<VbenIconButton class="mb-3">
<MdiGoogle />
</VbenIconButton>
</div> </div>
</div> </div>
</template> </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_AUDIENCE,
VITE_GLOB_CLIENT_ID, VITE_GLOB_CLIENT_ID,
VITE_GLOB_CLIENT_SECRET, VITE_GLOB_CLIENT_SECRET,
VITE_GLOB_ONLY_OIDC,
VITE_GLOB_UI_FRAMEWORK, VITE_GLOB_UI_FRAMEWORK,
} = config; } = config;
@ -30,6 +31,7 @@ export function useAppConfig(
audience: VITE_GLOB_AUDIENCE, audience: VITE_GLOB_AUDIENCE,
clientId: VITE_GLOB_CLIENT_ID, clientId: VITE_GLOB_CLIENT_ID,
clientSecret: VITE_GLOB_CLIENT_SECRET, clientSecret: VITE_GLOB_CLIENT_SECRET,
onlyOidc: VITE_GLOB_ONLY_OIDC === 'true',
uiFramework: VITE_GLOB_UI_FRAMEWORK, 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_CLIENT_SECRET: string;
VITE_GLOB_AUTHORITY: string; VITE_GLOB_AUTHORITY: string;
VITE_GLOB_AUDIENCE?: string; VITE_GLOB_AUDIENCE?: string;
VITE_GLOB_ONLY_OIDC?: string;
VITE_GLOB_UI_FRAMEWORK: string; VITE_GLOB_UI_FRAMEWORK: string;
} }
@ -22,6 +23,7 @@ export interface ApplicationConfig {
audience?: string; audience?: string;
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
onlyOidc?: boolean;
uiFramework: string; uiFramework: string;
} }

1
apps/vben5/pnpm-workspace.yaml

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

Loading…
Cancel
Save