feat: implement OAuth 2.0 login flow with auto-redirect

- Modify login page to auto-redirect to auth center
- Update auth store to use OAuth login flow
- Handle OAuth callback and token exchange
- Update logout to use OAuth logout endpoint
This commit is contained in:
Sam 2026-02-06 01:14:11 +08:00
parent 0ebe467927
commit 0003b0f1fd
3 changed files with 117 additions and 118 deletions

View File

@ -10,7 +10,8 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { ElNotification } from 'element-plus'; import { ElNotification } from 'element-plus';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api'; import { getAccessCodesApi, getUserInfoApi, logoutApi, oauthLogoutApi } from '#/api';
import { oauthService } from '#/services/auth/oauth';
import { $t } from '#/locales'; import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
@ -21,51 +22,105 @@ export const useAuthStore = defineStore('auth', () => {
const loginLoading = ref(false); const loginLoading = ref(false);
/** /**
* * OAuth 2.0 -
* Asynchronously handle the login process
* @param params
*/ */
async function authLogin( function authLogin(
params: Recordable<any>, params?: Recordable<any>,
onSuccess?: () => Promise<void> | void, onSuccess?: () => Promise<void> | void,
) { ) {
// 异步处理用户登录操作并获取 accessToken loginLoading.value = true;
let userInfo: null | UserInfo = null; try {
// OAuth 2.0 方式:重定向到认证中心
oauthService.initiateAuthorization();
} catch (error) {
ElNotification({
message: $t('authentication.loginFailed'),
title: $t('authentication.loginFailedTitle'),
type: 'error',
});
} finally {
loginLoading.value = false;
}
}
/**
* OAuth
*/
async function handleOAuthCallback(code: string, state: string) {
try { try {
loginLoading.value = true; loginLoading.value = true;
const { accessToken } = await loginApi(params);
// 如果成功获取到 accessToken await oauthService.handleCallback(code, state);
const accessToken = oauthService.getAccessToken();
if (accessToken) { if (accessToken) {
// 将 accessToken 存储到 accessStore 中
accessStore.setAccessToken(accessToken); accessStore.setAccessToken(accessToken);
// 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([ const [fetchUserInfoResult, accessCodes] = await Promise.all([
fetchUserInfo(), fetchUserInfo(),
getAccessCodesApi(), getAccessCodesApi(),
]); ]);
userInfo = fetchUserInfoResult; userStore.setUserInfo(fetchUserInfoResult);
userStore.setUserInfo(userInfo);
accessStore.setAccessCodes(accessCodes); accessStore.setAccessCodes(accessCodes);
if (accessStore.loginExpired) { if (fetchUserInfoResult?.realName) {
accessStore.setLoginExpired(false);
} else {
onSuccess
? await onSuccess?.()
: await router.push(
userInfo.homePath || preferences.app.defaultHomePath,
);
}
if (userInfo?.realName) {
ElNotification({ ElNotification({
message: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, message: `${$t('authentication.loginSuccessDesc')}:${fetchUserInfoResult?.realName}`,
title: $t('authentication.loginSuccess'), title: $t('authentication.loginSuccess'),
type: 'success', type: 'success',
});
}
await router.push(
fetchUserInfoResult.homePath || preferences.app.defaultHomePath,
);
}
} catch (error: any) {
ElNotification({
message: error.message || $t('authentication.loginFailed'),
title: $t('authentication.loginFailedTitle'),
type: 'error',
});
await router.replace(LOGIN_PATH);
} finally {
loginLoading.value = false;
}
}
async function logout(redirect: boolean = true) {
try {
// OAuth 登出
await oauthLogoutApi();
} catch {
// 不做任何处理
}
resetAllStores();
accessStore.setLoginExpired(false);
// OAuth 登出并重定向到认证中心
oauthService.logout();
}
async function fetchUserInfo() {
let userInfo: null | UserInfo = null;
userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo);
return userInfo;
}
function $reset() {
loginLoading.value = false;
}
return {
$reset,
authLogin,
fetchUserInfo,
handleOAuthCallback,
loginLoading,
logout,
};
}); });
} }
} }

View File

@ -1,98 +1,43 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui'; import { onMounted, ref } from 'vue';
import type { BasicOption } from '@vben/types';
import { computed, markRaw } from 'vue'; import { ElCard, ElIcon } from 'element-plus';
import { Loading } from '@element-plus/icons-vue';
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useAuthStore } from '#/store';
defineOptions({ name: 'Login' }); defineOptions({ name: 'Login' });
const authStore = useAuthStore(); const loading = ref(true);
const message = ref('正在跳转到认证中心...');
const MOCK_USER_OPTIONS: BasicOption[] = [ onMounted(() => {
{ // OAuth
label: 'Super', setTimeout(() => {
value: 'vben', window.location.href = import.meta.env.VITE_OAUTH_ISSUER + '/connect/authorize?' +
}, new URLSearchParams({
{ response_type: 'code',
label: 'Admin', client_id: import.meta.env.VITE_OAUTH_CLIENT_ID || 'fengling-console',
value: 'admin', redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URI || 'http://localhost:5777/callback',
}, scope: import.meta.env.VITE_OAUTH_SCOPES || 'openid profile email api',
{ state: Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2),
label: 'User', }).toString();
value: 'jack', }, 500);
},
];
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenSelect',
componentProps: {
options: MOCK_USER_OPTIONS,
placeholder: $t('authentication.selectAccount'),
},
fieldName: 'selectAccount',
label: $t('authentication.selectAccount'),
rules: z
.string()
.min(1, { message: $t('authentication.selectAccount') })
.optional()
.default('vben'),
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
dependencies: {
trigger(values, form) {
if (values.selectAccount) {
const findUser = MOCK_USER_OPTIONS.find(
(item) => item.value === values.selectAccount,
);
if (findUser) {
form.setValues({
password: '123456',
username: findUser.value,
});
}
}
},
triggerFields: ['selectAccount'],
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: markRaw(SliderCaptcha),
fieldName: 'captcha',
rules: z.boolean().refine((value) => value, {
message: $t('authentication.verifyRequiredTip'),
}),
},
];
}); });
</script> </script>
<template> <template>
<AuthenticationLogin <div class="flex min-h-screen items-center justify-center bg-gray-100 p-4">
:form-schema="formSchema" <el-card class="w-full max-w-md">
:loading="authStore.loginLoading" <template #header>
@submit="authStore.authLogin" <h1 class="text-2xl font-bold text-center">Fengling Console</h1>
/> <p class="text-center text-sm text-gray-500">管理后台登录</p>
</template>
<div class="flex flex-col items-center justify-center py-12">
<el-icon :size="60" class="text-blue-500">
<Loading />
</el-icon>
<p class="mt-4 text-center text-gray-600">{{ message }}</p>
</div>
</el-card>
</div>
</template> </template>

View File

@ -2,10 +2,11 @@
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElCard, ElResult, ElButton } from 'element-plus' import { ElMessage, ElCard, ElResult, ElButton } from 'element-plus'
import { oauthService } from '#/services/auth/oauth' import { useAuthStore } from '#/store'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const authStore = useAuthStore()
const loading = ref(true) const loading = ref(true)
const error = ref<string | null>(null) const error = ref<string | null>(null)
@ -18,9 +19,7 @@ onMounted(async () => {
throw new Error('No authorization code found') throw new Error('No authorization code found')
} }
await oauthService.handleCallback(code, state) await authStore.handleOAuthCallback(code, state)
ElMessage.success('登录成功')
router.push('/')
} catch (err: any) { } catch (err: any) {
error.value = err.message || '认证失败' error.value = err.message || '认证失败'
ElMessage.error(error.value) ElMessage.error(error.value)