From fa93d717250dd62a8c4c4524687878ee106ab546 Mon Sep 17 00:00:00 2001 From: Sam <315859133@qq.com> Date: Sat, 7 Feb 2026 17:47:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0OAuth2=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E9=85=8D=E7=BD=AE=E5=92=8C=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加OAuth2认证相关配置文件和服务实现,包括环境变量配置、PKCE流程支持、token管理等功能。主要变更: - 新增OAuth2配置文件 - 实现OAuth2服务层 - 更新请求拦截器支持token自动刷新 - 修改认证API和store以支持OAuth2流程 --- OIDC_INTEGRATION.md | 172 +++ apps/web-ele/.env.development | 5 +- apps/web-ele/package.json | 1 + apps/web-ele/src/api/core/auth.ts | 133 +- apps/web-ele/src/api/core/index.ts | 1 - apps/web-ele/src/api/core/oauth.ts | 155 --- apps/web-ele/src/api/core/user.ts | 2 +- apps/web-ele/src/api/fengling/oauth.ts | 76 +- apps/web-ele/src/api/request.ts | 64 +- apps/web-ele/src/config/oauth.ts | 22 - apps/web-ele/src/config/oidc.ts | 25 + apps/web-ele/src/router/guard.ts | 21 +- apps/web-ele/src/router/routes/core.ts | 10 + apps/web-ele/src/services/auth/oauth.ts | 158 --- .../web-ele/src/services/oidc-auth.service.ts | 201 +++ apps/web-ele/src/store/auth.ts | 263 ++-- .../views/_core/authentication/callback.vue | 84 +- .../src/views/_core/authentication/login.vue | 91 +- .../_core/authentication/logout-callback.vue | 51 + .../src/views/fengling/oauth-callback.vue | 49 - .../src/views/fengling/oauth/index.vue | 9 +- .../2026-02-06-oauth2-implementation-plan.md | 1232 +++++++++++++++++ pnpm-lock.yaml | 60 +- 23 files changed, 2217 insertions(+), 668 deletions(-) create mode 100644 OIDC_INTEGRATION.md delete mode 100644 apps/web-ele/src/api/core/oauth.ts delete mode 100644 apps/web-ele/src/config/oauth.ts create mode 100644 apps/web-ele/src/config/oidc.ts delete mode 100644 apps/web-ele/src/services/auth/oauth.ts create mode 100644 apps/web-ele/src/services/oidc-auth.service.ts create mode 100644 apps/web-ele/src/views/_core/authentication/logout-callback.vue delete mode 100644 apps/web-ele/src/views/fengling/oauth-callback.vue create mode 100644 docs/plans/2026-02-06-oauth2-implementation-plan.md diff --git a/OIDC_INTEGRATION.md b/OIDC_INTEGRATION.md new file mode 100644 index 0000000..d6c9a21 --- /dev/null +++ b/OIDC_INTEGRATION.md @@ -0,0 +1,172 @@ +# OIDC 集成说明 + +## 概述 + +本项目已经使用 `oidc-client-ts` 库集成 OpenID Connect 认证,替换了原有的自定义登录实现。 + +## 新增文件 + +### 1. OIDC 配置 +- `src/config/oidc.ts` - OIDC 客户端配置 + +### 2. OIDC 服务 +- `src/services/oidc-auth.service.ts` - OIDC 认证服务封装 + +### 3. 回调页面 +- `src/views/_core/authentication/callback.vue` - OIDC 登录回调处理页面 + +### 4. API 更新 +- `src/api/core/auth.ts` - 更新为使用 OIDC token + +### 5. Store 更新 +- `src/store/auth.ts` - 更新为使用 OIDC 认证流程 + +## 修改文件 + +### 1. 路由配置 +- `src/router/routes/core.ts` - 添加了 `/auth/callback` 路由 +- `src/router/guard.ts` - 更新为使用 OIDC 认证检查 + +### 2. 请求拦截器 +- `src/api/request.ts` - 更新为使用 OIDC 的 token 刷新 + +### 3. 环境配置 +- `.env.development` - 添加 `VITE_AUTH_SERVER_URL` 配置 + +## 环境变量 + +在 `.env` 文件中配置以下变量: + +```env +# OIDC认证服务器地址 +VITE_AUTH_SERVER_URL=http://localhost:5132 +``` + +## 认证流程 + +### 密码模式登录 (Resource Owner Password Credentials) + +1. 用户在登录页面输入用户名和密码 +2. 调用 `authStore.authLogin({ username, password })` +3. 使用 OIDC 的 `signinResourceOwnerCredentials` 方法直接获取 token +4. Token 存储到 store,并在请求头中携带 +5. 获取用户信息并设置到用户 store + +### 授权码模式登录 (Authorization Code Flow) + +1. 前端调用 `oidcAuthService.signinRedirect()` 重定向到认证服务器 +2. 用户在认证服务器完成认证 +3. 认证服务器重定向回 `/auth/callback?code=...` +4. 回调页面调用 `authStore.handleCallback()` 处理授权码 +5. 换取访问令牌并存储 + +## Token 管理 + +### 自动续期 + +- 当 token 即将过期(60秒前),OIDC 服务会自动静默续期 +- 如果静默续期失败,会重定向到登录页 + +### 手动刷新 + +```typescript +await authStore.refreshToken(); +``` + +### 退出登录 + +```typescript +await authStore.logout(); +``` + +## API 使用 + +### 使用 Access Token + +API 请求会自动携带 Bearer token: + +```typescript +// 请求头会自动添加 +Authorization: Bearer {access_token} +``` + +### 获取用户信息 + +```typescript +// 从 token claims 获取 +const userInfo = getUserInfoFromToken(user); + +// 或从服务器获取 +const userInfo = await getUserInfoApi(); +``` + +## 错误处理 + +### 常见错误 + +1. **invalid_grant** - 用户名或密码错误 +2. **invalid_client** - 客户端 ID 无效 +3. **expired_token** - Token 已过期 +4. **invalid_token** - Token 无效 + +所有错误都会显示友好的提示信息,并重定向到登录页。 + +## 注意事项 + +1. **Token 存储**:Token 存储在 localStorage 中,由 oidc-client-ts 管理 +2. **CORS 配置**:认证服务器需要配置 CORS 允许前端域名 +3. **HTTPS**:生产环境必须使用 HTTPS +4. **PKCE**:如果使用授权码模式,建议启用 PKCE 增强安全性 + +## 后端要求 + +认证服务器必须支持以下功能: + +- OpenID Connect 1.0 兼容 +- Resource Owner Password Credentials 授权类型 +- Authorization Code 授权类型 +- Refresh Token 支持 +- Userinfo 端点 + +## 开发建议 + +1. 开发时使用 `http://localhost:5132` 作为认证服务器 +2. 生产环境配置实际的认证服务器地址 +3. 使用浏览器开发者工具的 Application 标签查看存储的 token +4. 监控网络请求,查看 OAuth 流程 + +## 迁移指南 + +如果你之前使用自定义的登录实现: + +1. 删除旧的登录 API 调用 +2. 替换为使用 `authStore.authLogin()` 方法 +3. 确保 request 拦截器使用 OIDC token +4. 添加 `/auth/callback` 路由 +5. 配置环境变量 `VITE_AUTH_SERVER_URL` + +## 故障排除 + +### Token 过期问题 + +如果遇到 token 频繁过期: + +1. 检查认证服务器的 token 有效期设置 +2. 确保自动续期功能正常工作 +3. 查看浏览器控制台的错误信息 + +### CORS 问题 + +如果遇到 CORS 错误: + +1. 确保认证服务器配置了 CORS +2. 检查前端域名是否在允许列表中 +3. 确认 `VITE_AUTH_SERVER_URL` 配置正确 + +### 回调失败 + +如果登录回调失败: + +1. 检查 `redirect_uri` 是否匹配 +2. 确认认证服务器返回的授权码 +3. 查看浏览器控制台的详细错误信息 diff --git a/apps/web-ele/.env.development b/apps/web-ele/.env.development index f3e3b04..95c8d24 100644 --- a/apps/web-ele/.env.development +++ b/apps/web-ele/.env.development @@ -6,7 +6,10 @@ VITE_BASE=/ # 接口地址 - Fengling Console Backend VITE_GLOB_API_URL=http://localhost:5231/api -# OAuth 认证中心配置 +# OIDC认证服务器地址 +VITE_AUTH_SERVER_URL=http://localhost:5132 + +# OAuth 认证中心配置(旧的,保留兼容性) VITE_AUTH_SERVICE_URL=http://localhost:5132 VITE_OAUTH_CLIENT_ID=fengling-console VITE_OAUTH_REDIRECT_URI=http://localhost:5777/auth/callback diff --git a/apps/web-ele/package.json b/apps/web-ele/package.json index abbedb6..da33b8c 100644 --- a/apps/web-ele/package.json +++ b/apps/web-ele/package.json @@ -43,6 +43,7 @@ "@vueuse/core": "catalog:", "dayjs": "catalog:", "element-plus": "catalog:", + "oidc-client-ts": "^3.4.1", "pinia": "catalog:", "vue": "catalog:", "vue-router": "catalog:" diff --git a/apps/web-ele/src/api/core/auth.ts b/apps/web-ele/src/api/core/auth.ts index e31e0c5..fc3cecc 100644 --- a/apps/web-ele/src/api/core/auth.ts +++ b/apps/web-ele/src/api/core/auth.ts @@ -1,61 +1,110 @@ +import type { User } from 'oidc-client-ts'; import { baseRequestClient } from '#/api/request'; -import { oauthConfig } from '#/config/oauth'; +import { ElNotification } from 'element-plus'; export namespace AuthApi { - export interface TokenResponse { - access_token: string; - refresh_token: string; - token_type: string; - expires_in: number; - scope: string; + /** 用户信息 */ + export interface UserInfo { + id: string; + username: string; + email: string; + realName: string; + tenantId: string; + tenantCode: string; + tenantName: string; + roles: string[]; + homePath?: string; } - export interface UserInfo { - sub: string; - name: string; - email: string; - role: string[]; - tenant_id: string; + /** 登录参数 */ + export interface LoginParams { + password: string; + username: string; + } + + /** OIDC用户 */ + export type OidcUser = User; +} + +/** + * 获取用户信息 + * Get user info from token claims + */ +export function getUserInfoFromToken(user: AuthApi.OidcUser): AuthApi.UserInfo { + const profile = user.profile as any; + + return { + id: profile.sub || user.profile.sub, + username: profile.preferred_username || profile.name, + email: profile.email || '', + realName: profile.name || profile.preferred_username, + tenantId: profile.tenant_id, + tenantCode: profile.tenant_code, + tenantName: profile.tenant_name, + roles: (profile.role || []) as string[], + }; +} + +/** + * 从服务器获取用户详细信息 + * Fetch detailed user info from server + */ +export async function getUserInfoApi(): Promise { + try { + return await baseRequestClient.get('/connect/userinfo', { + withCredentials: true, + }); + } catch (error: any) { + // 如果token无效,返回错误 + if (error?.response?.status === 401) { + ElNotification({ + title: '会话已过期', + message: '您的登录会话已过期,请重新登录', + type: 'error', + }); + throw new Error('Unauthorized'); + } + throw error; } } +/** + * 退出登录 + */ +export async function logoutApi() { + try { + await baseRequestClient.post('/connect/logout', { + withCredentials: true, + }); + } catch (error) { + // 静默处理退出错误 + console.error('[Auth] Logout error:', error); + } +} + +/** + * Parse JWT token (legacy, for backward compatibility) + */ export function parseJwt(token: string): AuthApi.UserInfo | null { try { const parts = token.split('.'); if (parts.length !== 3) return null; - const payload = JSON.parse(atob(parts[1])); + const payloadStr = parts[1]; + if (!payloadStr) return null; + + const payload = JSON.parse(atob(payloadStr)); return { - sub: payload.sub, - name: payload.name || payload['unique_name'], - email: payload.email, - role: Array.isArray(payload.role) ? payload.role : [payload.role].filter(Boolean), - tenant_id: payload.tenant_id || payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'], + id: payload.sub, + username: payload.preferred_username || payload.name, + email: payload.email || '', + realName: payload.name || payload.preferred_username, + tenantId: payload.tenant_id, + tenantCode: payload.tenant_code, + tenantName: payload.tenant_name, + roles: Array.isArray(payload.role) ? payload.role : [payload.role].filter(Boolean), }; } catch { return null; } } - -export async function logoutApi(refreshToken: string) { - const response = await fetch(`${oauthConfig.authUrl}${oauthConfig.endpoints.revocation}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - token: refreshToken, - token_type_hint: 'refresh_token', - client_id: oauthConfig.clientId, - }), - }); - - if (!response.ok) { - const error = await response.text(); - console.error('Failed to revoke token:', error); - } -} - -export async function validateTokenApi() { - return baseRequestClient.get('/health'); -} diff --git a/apps/web-ele/src/api/core/index.ts b/apps/web-ele/src/api/core/index.ts index 1464f50..28a5aef 100644 --- a/apps/web-ele/src/api/core/index.ts +++ b/apps/web-ele/src/api/core/index.ts @@ -1,4 +1,3 @@ export * from './auth'; export * from './menu'; -export * from './oauth'; export * from './user'; diff --git a/apps/web-ele/src/api/core/oauth.ts b/apps/web-ele/src/api/core/oauth.ts deleted file mode 100644 index 24d3b4a..0000000 --- a/apps/web-ele/src/api/core/oauth.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { oauthConfig } from '#/config/oauth'; - -export interface TokenResponse { - access_token: string; - refresh_token: string; - token_type: string; - expires_in: number; - scope: string; -} - -export function generateRandomString(length: number = 128): string { - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; - let result = ''; - const array = new Uint8Array(length); - crypto.getRandomValues(array); - for (let i = 0; i < length; i++) { - result += chars[array[i] % chars.length]; - } - return result; -} - -export function generateCodeVerifier(): string { - return generateRandomString(128); -} - -export async function generateCodeChallenge(verifier: string): Promise { - const encoder = new TextEncoder(); - const data = encoder.encode(verifier); - const hash = await crypto.subtle.digest('SHA-256', data); - return btoa(String.fromCharCode(...new Uint8Array(hash))) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/, ''); -} - -export function generateState(): string { - return generateRandomString(32); -} - -export function storeOAuthState(codeVerifier: string, state: string, returnTo?: string) { - sessionStorage.setItem(oauthConfig.storageKeys.codeVerifier, codeVerifier); - sessionStorage.setItem(oauthConfig.storageKeys.state, state); - if (returnTo) { - sessionStorage.setItem(oauthConfig.storageKeys.returnTo, returnTo); - } -} - -export function getAndClearOAuthState() { - const codeVerifier = sessionStorage.getItem(oauthConfig.storageKeys.codeVerifier); - const state = sessionStorage.getItem(oauthConfig.storageKeys.state); - const returnTo = sessionStorage.getItem(oauthConfig.storageKeys.returnTo); - - sessionStorage.removeItem(oauthConfig.storageKeys.codeVerifier); - sessionStorage.removeItem(oauthConfig.storageKeys.state); - sessionStorage.removeItem(oauthConfig.storageKeys.returnTo); - - return { codeVerifier, state, returnTo }; -} - -export function redirectToAuth(returnTo?: string) { - const codeVerifier = generateCodeVerifier(); - const state = generateState(); - - generateCodeChallenge(codeVerifier).then((codeChallenge) => { - storeOAuthState(codeVerifier, state, returnTo); - - const params = new URLSearchParams({ - client_id: oauthConfig.clientId, - redirect_uri: oauthConfig.redirectUri, - response_type: 'code', - scope: oauthConfig.scope, - code_challenge: codeChallenge, - code_challenge_method: 'S256', - state: state, - }); - - window.location.href = `${oauthConfig.authUrl}${oauthConfig.endpoints.authorize}?${params.toString()}`; - }); -} - -export async function exchangeCodeForToken(code: string, state: string): Promise { - const { codeVerifier } = getAndClearOAuthState(); - - if (!codeVerifier) { - throw new Error('Code verifier not found. OAuth2 state may be corrupted.'); - } - - const response = await fetch(`${oauthConfig.authUrl}${oauthConfig.endpoints.token}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - grant_type: 'authorization_code', - code: code, - redirect_uri: oauthConfig.redirectUri, - client_id: oauthConfig.clientId, - code_verifier: codeVerifier, - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Failed to exchange code for token: ${error}`); - } - - return await response.json(); -} - -export async function refreshAccessToken(refreshToken: string): Promise { - const response = await fetch(`${oauthConfig.authUrl}${oauthConfig.endpoints.token}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: refreshToken, - client_id: oauthConfig.clientId, - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Failed to refresh token: ${error}`); - } - - return await response.json(); -} - -export async function revokeToken(token: string) { - const response = await fetch(`${oauthConfig.authUrl}${oauthConfig.endpoints.revocation}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - token: token, - token_type_hint: 'refresh_token', - client_id: oauthConfig.clientId, - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Failed to revoke token: ${error}`); - } -} - -export function getLogoutUrl(): string { - const params = new URLSearchParams({ - post_logout_redirect_uri: oauthConfig.redirectUri, - }); - return `${oauthConfig.authUrl}${oauthConfig.endpoints.logout}?${params.toString()}`; -} diff --git a/apps/web-ele/src/api/core/user.ts b/apps/web-ele/src/api/core/user.ts index 7e28ea8..806b27f 100644 --- a/apps/web-ele/src/api/core/user.ts +++ b/apps/web-ele/src/api/core/user.ts @@ -5,6 +5,6 @@ import { requestClient } from '#/api/request'; /** * 获取用户信息 */ -export async function getUserInfoApi() { +export async function getUserInfoOldApi() { return requestClient.get('/user/info'); } diff --git a/apps/web-ele/src/api/fengling/oauth.ts b/apps/web-ele/src/api/fengling/oauth.ts index 9252b3c..4b694b5 100644 --- a/apps/web-ele/src/api/fengling/oauth.ts +++ b/apps/web-ele/src/api/fengling/oauth.ts @@ -1,45 +1,69 @@ import type { FenglingApi } from './typings'; - -import { requestClient } from '#/api/request'; +import { baseRequestClient } from '#/api/request'; export namespace OAuthApi { + /** + * 获取OAuth客户端列表 + */ export async function getClientList(params: { - page?: number - pageSize?: number - keyword?: string - status?: string - }) { - return requestClient.get>( - '/oauth/clients', - { params } + page: number; + pageSize: number; + keyword?: string; + }): Promise> { + const queryParams = new URLSearchParams({ + page: params.page.toString(), + pageSize: params.pageSize.toString(), + ...(params.keyword ? { keyword: params.keyword } : {}), + }); + return await baseRequestClient.get>( + `/oauth/clients?${queryParams.toString()}`, ); } - export async function getClientById(id: number) { - return requestClient.get(`/oauth/clients/${id}`); + /** + * 获取OAuth客户端详情 + */ + export async function getClient(id: number): Promise { + return await baseRequestClient.get(`/oauth/clients/${id}`); } - export async function createClient(data: FenglingApi.OAuth.CreateOAuthClientDto) { - return requestClient.post('/oauth/clients', data); + /** + * 创建OAuth客户端 + */ + export async function createClient( + data: FenglingApi.OAuth.CreateOAuthClientDto, + ): Promise { + return await baseRequestClient.post('/oauth/clients', data); } - export async function updateClient(id: number, data: FenglingApi.OAuth.UpdateOAuthClientDto) { - return requestClient.put(`/oauth/clients/${id}`, data); + /** + * 更新OAuth客户端 + */ + export async function updateClient( + id: number, + data: FenglingApi.OAuth.UpdateOAuthClientDto, + ): Promise { + return await baseRequestClient.put(`/oauth/clients/${id}`, data); } - export async function deleteClient(id: number) { - return requestClient.delete(`/oauth/clients/${id}`); + /** + * 删除OAuth客户端 + */ + export async function deleteClient(id: number): Promise { + await baseRequestClient.delete(`/oauth/clients/${id}`); } - export async function regenerateClientSecret(id: number) { - return requestClient.post(`/oauth/clients/${id}/regenerate-secret`); + /** + * 重置OAuth客户端密钥 + */ + export async function resetClientSecret(id: number): Promise<{ clientSecret: string }> { + return await baseRequestClient.post<{ clientSecret: string }>(`/oauth/clients/${id}/reset-secret`); } - export async function activateClient(id: number) { - return requestClient.post(`/oauth/clients/${id}/activate`); - } - - export async function deactivateClient(id: number) { - return requestClient.post(`/oauth/clients/${id}/deactivate`); + /** + * 启用/禁用OAuth客户端 + */ + export async function toggleClientStatus(id: number, status: 'active' | 'inactive'): Promise { + await baseRequestClient.put(`/oauth/clients/${id}/status`, { status }); } } diff --git a/apps/web-ele/src/api/request.ts b/apps/web-ele/src/api/request.ts index 7c24a1d..fd60118 100644 --- a/apps/web-ele/src/api/request.ts +++ b/apps/web-ele/src/api/request.ts @@ -1,4 +1,3 @@ -import { oauthConfig } from '#/config/oauth'; import type { RequestClientOptions } from '@vben/request'; import { useAppConfig } from '@vben/hooks'; @@ -15,8 +14,6 @@ import { ElMessage } from 'element-plus'; import { useAuthStore } from '#/store'; -import { refreshAccessToken } from './core/oauth'; - const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); function createRequestClient(baseURL: string, options?: RequestClientOptions) { @@ -25,13 +22,11 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { baseURL, }); - function isTokenExpired(expiresAt: number | null): boolean { - if (!expiresAt) return true; - return Date.now() >= expiresAt - 5 * 60 * 1000; - } - + /** + * 重新认证逻辑 + */ async function doReAuthenticate() { - console.warn('Access token or refresh token is invalid or expired.'); + console.warn('Access token or refresh token is invalid or expired. '); const accessStore = useAccessStore(); const authStore = useAuthStore(); accessStore.setAccessToken(null); @@ -45,24 +40,24 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { } } - async function doRefreshToken() { + /** + * 刷新token逻辑 - 使用OIDC + */ + async function doRefreshToken(): Promise { const accessStore = useAccessStore(); - const refreshToken = accessStore.refreshToken; - - if (!refreshToken) { - await doReAuthenticate(); - throw new Error('No refresh token available'); - } - + const authStore = useAuthStore(); try { - const tokenResponse = await refreshAccessToken(refreshToken); - accessStore.setAccessToken(tokenResponse.access_token); - accessStore.setRefreshToken(tokenResponse.refresh_token); - accessStore.setExpiresAt(Date.now() + tokenResponse.expires_in * 1000); - return tokenResponse.access_token; + // 使用OIDC的刷新token方法 + await authStore.refreshToken(); + // 刷新后重新获取token + const newToken = await authStore.getAccessToken(); + if (newToken) { + accessStore.setAccessToken(newToken); + return newToken; + } + throw new Error('Failed to get access token after refresh'); } catch (error) { - console.error('Failed to refresh token:', error); - await doReAuthenticate(); + console.error('[Request] Refresh token failed:', error); throw error; } } @@ -71,28 +66,18 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { return token ? `Bearer ${token}` : null; } + // 请求头处理 client.addRequestInterceptor({ fulfilled: async (config) => { const accessStore = useAccessStore(); - if (accessStore.accessToken && accessStore.expiresAt) { - if (isTokenExpired(accessStore.expiresAt)) { - try { - const newToken = await doRefreshToken(); - config.headers.Authorization = formatToken(newToken); - } catch { - config.headers.Authorization = formatToken(null); - } - } else { - config.headers.Authorization = formatToken(accessStore.accessToken); - } - } - + config.headers.Authorization = formatToken(accessStore.accessToken); config.headers['Accept-Language'] = preferences.app.locale; return config; }, }); + // 处理返回的响应数据格式 client.addResponseInterceptor( defaultResponseInterceptor({ codeField: 'code', @@ -101,6 +86,7 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { }), ); + // token过期的处理 client.addResponseInterceptor( authenticateResponseInterceptor({ client, @@ -111,10 +97,14 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { }), ); + // 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里 client.addResponseInterceptor( errorMessageResponseInterceptor((msg: string, error) => { + // 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg + // 当前mock接口返回的错误字段是 error 或者 message const responseData = error?.response?.data ?? {}; const errorMessage = responseData?.error ?? responseData?.message ?? ''; + // 如果没有错误信息,则会根据状态码进行提示 ElMessage.error(errorMessage || msg); }), ); diff --git a/apps/web-ele/src/config/oauth.ts b/apps/web-ele/src/config/oauth.ts deleted file mode 100644 index c508d14..0000000 --- a/apps/web-ele/src/config/oauth.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const oauthConfig = { - clientId: import.meta.env.VITE_OAUTH_CLIENT_ID || 'fengling-console', - redirectUri: import.meta.env.VITE_OAUTH_REDIRECT_URI || `${window.location.origin}/auth/callback`, - authUrl: import.meta.env.VITE_AUTH_SERVICE_URL || 'http://localhost:5000', - scope: import.meta.env.VITE_OAUTH_SCOPE || 'api offline_access openid profile email', - - endpoints: { - authorize: '/connect/authorize', - token: '/connect/token', - logout: '/connect/logout', - revocation: '/connect/revocation', - }, - - storageKeys: { - accessToken: 'oauth_access_token', - refreshToken: 'oauth_refresh_token', - expiresAt: 'oauth_expires_at', - codeVerifier: 'oauth_code_verifier', - state: 'oauth_state', - returnTo: 'oauth_return_to', - }, -}; diff --git a/apps/web-ele/src/config/oidc.ts b/apps/web-ele/src/config/oidc.ts new file mode 100644 index 0000000..9d17a9b --- /dev/null +++ b/apps/web-ele/src/config/oidc.ts @@ -0,0 +1,25 @@ +import type { UserManager, UserManagerSettings } from 'oidc-client-ts'; + +/** + * OIDC 配置 + * OIDC Configuration + */ +export const oidcConfig: UserManagerSettings = { + authority: import.meta.env.VITE_AUTH_SERVER_URL || 'http://localhost:5132', + client_id: 'fengling-console', + redirect_uri: window.location.origin + '/auth/callback', + post_logout_redirect_uri: window.location.origin + '/auth/logout-callback', + response_type: 'code', + scope: 'openid profile email api offline_access', + automaticSilentRenew: true, + includeIdTokenInSilentRenew: false, + loadUserInfo: true, + filterProtocolClaims: true, + accessTokenExpiringNotificationTimeInSeconds: 60, + silentRequestTimeoutInSeconds: 10000, + silent_redirect_uri: window.location.origin + '/silent-renew.html', + checkSessionIntervalInSeconds: 60000, + query_status_response_type: 'fragment', +}; + +export type { UserManagerSettings, UserManager }; diff --git a/apps/web-ele/src/router/guard.ts b/apps/web-ele/src/router/guard.ts index a1ad6d8..59ced49 100644 --- a/apps/web-ele/src/router/guard.ts +++ b/apps/web-ele/src/router/guard.ts @@ -62,8 +62,9 @@ function setupAccessGuard(router: Router) { return true; } - // accessToken 检查 - if (!accessStore.accessToken) { + // 使用OIDC检查认证状态 + const isAuthenticated = await authStore.checkAuth(); + if (!isAuthenticated) { // 明确声明忽略权限访问权限,则可以访问 if (to.meta.ignoreAccess) { return true; @@ -85,6 +86,14 @@ function setupAccessGuard(router: Router) { return to; } + // 检查store中的accessToken是否存在,如果不存在,从OIDC获取 + if (!accessStore.accessToken) { + const token = await authStore.getAccessToken(); + if (token) { + accessStore.setAccessToken(token); + } + } + // 是否已经生成过动态路由 if (accessStore.isAccessChecked) { return true; @@ -92,8 +101,8 @@ function setupAccessGuard(router: Router) { // 生成路由表 // 当前登录用户拥有的角色标识列表 - const userInfo = userStore.userInfo || (await authStore.fetchUserInfo()); - const userRoles = userInfo.roles ?? []; + const userInfo = userStore.userInfo; + const userRoles = userInfo?.roles ?? []; // 生成菜单和路由 const { accessibleMenus, accessibleRoutes } = await generateAccess({ @@ -107,9 +116,11 @@ function setupAccessGuard(router: Router) { accessStore.setAccessMenus(accessibleMenus); accessStore.setAccessRoutes(accessibleRoutes); accessStore.setIsAccessChecked(true); + + const homePath = userInfo?.homePath || preferences.app.defaultHomePath; const redirectPath = (from.query.redirect ?? (to.path === preferences.app.defaultHomePath - ? userInfo.homePath || preferences.app.defaultHomePath + ? homePath : to.fullPath)) as string; return { diff --git a/apps/web-ele/src/router/routes/core.ts b/apps/web-ele/src/router/routes/core.ts index 32612fc..295082c 100644 --- a/apps/web-ele/src/router/routes/core.ts +++ b/apps/web-ele/src/router/routes/core.ts @@ -102,6 +102,16 @@ const coreRoutes: RouteRecordRaw[] = [ title: 'OAuth Callback', }, }, + { + name: 'LogoutCallback', + path: '/auth/logout-callback', + component: () => import('#/views/_core/authentication/logout-callback.vue'), + meta: { + hideInMenu: true, + ignoreAuth: true, + title: 'Logout Callback', + }, + }, ]; export { coreRoutes, fallbackNotFoundRoute }; diff --git a/apps/web-ele/src/services/auth/oauth.ts b/apps/web-ele/src/services/auth/oauth.ts deleted file mode 100644 index a144300..0000000 --- a/apps/web-ele/src/services/auth/oauth.ts +++ /dev/null @@ -1,158 +0,0 @@ -interface OAuthConfig { - issuer: string - clientId: string - redirectUri: string - scopes: string -} - -interface TokenResponse { - access_token: string - token_type: string - expires_in: number - refresh_token?: string - scope?: string -} - -interface TokenInfo { - accessToken: string - refreshToken?: string - expiresAt: number -} - -export class OAuthService { - private config: OAuthConfig - private tokenInfo: TokenInfo | null = null - private tokenKey = 'oauth_token' - - constructor() { - this.config = { - issuer: import.meta.env.VITE_OAUTH_ISSUER || 'https://auth.fengling.local', - clientId: import.meta.env.VITE_OAUTH_CLIENT_ID || 'fengling-console', - redirectUri: import.meta.env.VITE_OAUTH_REDIRECT_URI || 'http://localhost:5777/callback', - scopes: import.meta.env.VITE_OAUTH_SCOPES || 'openid profile email api', - } - this.loadToken() - } - - private loadToken() { - const savedToken = localStorage.getItem(this.tokenKey) - if (savedToken) { - this.tokenInfo = JSON.parse(savedToken) - } - } - - private saveToken(tokenInfo: TokenInfo) { - this.tokenInfo = tokenInfo - localStorage.setItem(this.tokenKey, JSON.stringify(tokenInfo)) - } - - getAccessToken(): string | null { - if (!this.tokenInfo) { - return null - } - - if (this.tokenInfo.expiresAt < Date.now()) { - return null - } - - return this.tokenInfo.accessToken - } - - isAuthenticated(): boolean { - return this.getAccessToken() !== null - } - - initiateAuthorization(): void { - const params = new URLSearchParams({ - response_type: 'code', - client_id: this.config.clientId, - redirect_uri: this.config.redirectUri, - scope: this.config.scopes, - state: this.generateState(), - }) - - const authUrl = `${this.config.issuer}/connect/authorize?${params.toString()}` - localStorage.setItem('oauth_state', params.get('state')!) - window.location.href = authUrl - } - - async handleCallback(code: string, state: string): Promise { - const savedState = localStorage.getItem('oauth_state') - if (!savedState || savedState !== state) { - throw new Error('Invalid state parameter') - } - - localStorage.removeItem('oauth_state') - - const tokenResponse = await this.exchangeCodeForToken(code) - this.saveToken({ - accessToken: tokenResponse.access_token, - refreshToken: tokenResponse.refresh_token, - expiresAt: Date.now() + tokenResponse.expires_in * 1000, - }) - } - - private async exchangeCodeForToken(code: string): Promise { - const response = await fetch(`${this.config.issuer}/connect/token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - grant_type: 'authorization_code', - code, - client_id: this.config.clientId, - redirect_uri: this.config.redirectUri, - }), - }) - - if (!response.ok) { - throw new Error('Failed to exchange code for token') - } - - return response.json() - } - - async refreshToken(): Promise { - if (!this.tokenInfo?.refreshToken) { - throw new Error('No refresh token available') - } - - const response = await fetch(`${this.config.issuer}/connect/token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: this.tokenInfo.refreshToken, - client_id: this.config.clientId, - }), - }) - - if (!response.ok) { - throw new Error('Failed to refresh token') - } - - const tokenResponse: TokenResponse = await response.json() - this.saveToken({ - accessToken: tokenResponse.access_token, - refreshToken: tokenResponse.refresh_token || this.tokenInfo.refreshToken, - expiresAt: Date.now() + tokenResponse.expires_in * 1000, - }) - } - - logout(): void { - this.tokenInfo = null - localStorage.removeItem(this.tokenKey) - localStorage.removeItem('oauth_state') - const logoutUrl = `${this.config.issuer}/account/logout?post_logout_redirect_uri=${encodeURIComponent(window.location.origin)}` - window.location.href = logoutUrl - } - - private generateState(): string { - return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) - } -} - -export const oauthService = new OAuthService() diff --git a/apps/web-ele/src/services/oidc-auth.service.ts b/apps/web-ele/src/services/oidc-auth.service.ts new file mode 100644 index 0000000..59a1f75 --- /dev/null +++ b/apps/web-ele/src/services/oidc-auth.service.ts @@ -0,0 +1,201 @@ +import type { User } from 'oidc-client-ts'; +import { UserManager, WebStorageStateStore } from 'oidc-client-ts'; +import { oidcConfig } from '#/config/oidc'; + +/** + * OIDC 服务 + * OIDC Service for handling authentication + */ +class OidcAuthService { + private userManager: UserManager; + private static instance: OidcAuthService; + + private constructor() { + this.userManager = new UserManager({ + ...oidcConfig, + userStore: new WebStorageStateStore({ store: window.localStorage }), + }); + + this.setupEventListeners(); + } + + public static getInstance(): OidcAuthService { + if (!OidcAuthService.instance) { + OidcAuthService.instance = new OidcAuthService(); + } + return OidcAuthService.instance; + } + + /** + * 设置事件监听器 + * Setup event listeners for token events + */ + private setupEventListeners() { + this.userManager.events.addAccessTokenExpiring(() => { + console.log('[OIDC] Access token expiring...'); + this.silentRenew().catch((error) => { + console.error('[OIDC] Silent renew failed:', error); + }); + }); + + this.userManager.events.addAccessTokenExpired(() => { + console.log('[OIDC] Access token expired'); + window.location.reload(); + }); + + this.userManager.events.addSilentRenewError((error) => { + console.error('[OIDC] Silent renew error:', error); + }); + + this.userManager.events.addUserSignedOut(() => { + console.log('[OIDC] User signed out'); + this.signinRedirect(); + }); + } + + /** + * 获取当前用户 + * Get current user + */ + public async getUser(): Promise { + try { + return await this.userManager.getUser(); + } catch (error) { + console.error('[OIDC] Error getting user:', error); + return null; + } + } + + /** + * 重定向到登录页面 + * Redirect to login page + */ + public async signinRedirect(): Promise { + try { + await this.userManager.signinRedirect(); + } catch (error) { + console.error('[OIDC] Signin redirect error:', error); + throw error; + } + } + + /** + * 处理登录回调 + * Handle login callback + */ + public async handleSigninCallback(): Promise { + try { + const user = await this.userManager.signinRedirectCallback(); + console.log('[OIDC] Signin callback successful:', user); + return user; + } catch (error) { + console.error('[OIDC] Signin callback error:', error); + throw error; + } + } + + /** + * 密码模式登录(直接使用username/password) + * Password grant login (direct username/password) + */ + public async signinPassword( + username: string, + password: string, + ): Promise { + try { + const user = await this.userManager.signinResourceOwnerCredentials({ + username, + password, + }); + console.log('[OIDC] Signin with password successful:', user); + return user; + } catch (error) { + console.error('[OIDC] Signin with password error:', error); + throw error; + } + } + + /** + * 静默续期token + * Silent renew token + */ + public async silentRenew(): Promise { + try { + const user = await this.userManager.signinSilent(); + if (user) { + console.log('[OIDC] Silent renew successful:', user); + return user; + } + throw new Error('Silent renew returned null'); + } catch (error) { + console.error('[OIDC] Silent renew error:', error); + throw error; + } + } + + /** + * 移除用户并重定向到登录页 + * Remove user and redirect to login + */ + public async signoutRedirect(): Promise { + try { + await this.userManager.signoutRedirect(); + } catch (error) { + console.error('[OIDC] Signout redirect error:', error); + throw error; + } + } + + /** + * 移除用户但不重定向 + * Remove user without redirect + */ + public async removeUser(): Promise { + try { + await this.userManager.removeUser(); + console.log('[OIDC] User removed'); + } catch (error) { + console.error('[OIDC] Remove user error:', error); + throw error; + } + } + + /** + * 获取访问令牌 + * Get access token + */ + public async getAccessToken(): Promise { + const user = await this.getUser(); + return user?.access_token ?? null; + } + + /** + * 检查用户是否已认证 + * Check if user is authenticated + */ + public async isAuthenticated(): Promise { + const user = await this.getUser(); + return user !== null && !user.expired; + } + + /** + * 刷新令牌 + * Refresh token + */ + public async refreshToken(): Promise { + try { + const user = await this.userManager.signinSilent(); + if (user) { + console.log('[OIDC] Token refreshed:', user); + return user; + } + throw new Error('Refresh token returned null'); + } catch (error) { + console.error('[OIDC] Refresh token error:', error); + throw error; + } + } +} + +export const oidcAuthService = OidcAuthService.getInstance(); +export default oidcAuthService; diff --git a/apps/web-ele/src/store/auth.ts b/apps/web-ele/src/store/auth.ts index 7d56fcf..e42e37f 100644 --- a/apps/web-ele/src/store/auth.ts +++ b/apps/web-ele/src/store/auth.ts @@ -1,7 +1,7 @@ -import type { UserInfo } from '@vben/types'; +import type { User as OidcUser } from 'oidc-client-ts'; import { ref } from 'vue'; -import { useRouter } from 'vue-router'; +import { useRouter, useRoute } from 'vue-router'; import { LOGIN_PATH } from '@vben/constants'; import { preferences } from '@vben/preferences'; @@ -10,123 +10,222 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; import { ElNotification } from 'element-plus'; import { defineStore } from 'pinia'; -import { parseJwt, logoutApi } from '#/api/core/auth'; -import { exchangeCodeForToken, getLogoutUrl, redirectToAuth } from '#/api/core/oauth'; -import { $t } from '#/locales'; - -export interface OAuth2State { - code: string; - state: string; -} +import { getUserInfoApi, getUserInfoFromToken, logoutApi } from '#/api/core/auth'; +import oidcAuthService from '#/services/oidc-auth.service'; export const useAuthStore = defineStore('auth', () => { const accessStore = useAccessStore(); const userStore = useUserStore(); const router = useRouter(); + const route = useRoute(); const loginLoading = ref(false); + const oidcUser = ref(null); - async function handleOAuthCallback(oauthState: OAuth2State) { - let userInfo: null | UserInfo = null; + /** + * 获取访问令牌 + * Get access token from OIDC + */ + async function getAccessToken(): Promise { try { - loginLoading.value = true; - - const tokenResponse = await exchangeCodeForToken(oauthState.code, oauthState.state); - - accessStore.setAccessToken(tokenResponse.access_token); - accessStore.setRefreshToken(tokenResponse.refresh_token); - accessStore.setExpiresAt(Date.now() + tokenResponse.expires_in * 1000); - - const jwtClaims = parseJwt(tokenResponse.access_token); - if (jwtClaims) { - userInfo = { - userId: jwtClaims.sub, - username: jwtClaims.name, - realName: jwtClaims.name, - email: jwtClaims.email, - roles: jwtClaims.role, - homePath: preferences.app.defaultHomePath, - }; - - userStore.setUserInfo(userInfo); - accessStore.setAccessCodes(jwtClaims.role || []); - } - - if (accessStore.loginExpired) { - accessStore.setLoginExpired(false); - } else { - const returnTo = sessionStorage.getItem('oauth_return_to') || preferences.app.defaultHomePath; - sessionStorage.removeItem('oauth_return_to'); - await router.push(returnTo); - } - - if (userInfo?.realName) { - ElNotification({ - message: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, - title: $t('authentication.loginSuccess'), - type: 'success', - }); - } - } catch (error: any) { - console.error('OAuth2 callback error:', error); - ElNotification({ - message: error.message || '认证失败', - title: '登录失败', - type: 'error', - }); - await router.push(LOGIN_PATH); - } finally { - loginLoading.value = false; + return await oidcAuthService.getAccessToken(); + } catch (error) { + console.error('[Auth] Get access token error:', error); + return null; } + } + /** + * 授权码模式登录 + * Authorization code login + */ + async function authLogin( + _params: {}, + _onSuccess?: () => Promise | void, + ) { + // 重定向到 OIDC 授权端点 + await oidcAuthService.signinRedirect(); return { - userInfo, + userInfo: null, }; } - async function authLogin( - params: any = {}, - onSuccess?: () => Promise | void, - ) { - const returnTo = params.returnTo || router.currentRoute.value.fullPath; - redirectToAuth(returnTo); - return { userInfo: null }; + /** + * 处理OIDC回调 + * Handle OIDC callback + */ + async function handleCallback() { + try { + const user = await oidcAuthService.handleSigninCallback(); + oidcUser.value = user; + + accessStore.setAccessToken(user.access_token); + + let userInfo = getUserInfoFromToken(user); + + const basicUserInfo = { + userId: userInfo.id, + username: userInfo.username, + realName: userInfo.realName, + avatar: '', + token: user.access_token, + }; + userStore.setUserInfo(basicUserInfo); + + try { + const detailedUserInfo = await getUserInfoApi(); + const detailedBasicUserInfo = { + userId: detailedUserInfo.id, + username: detailedUserInfo.username, + realName: detailedUserInfo.realName, + avatar: '', + token: user.access_token, + }; + userStore.setUserInfo(detailedBasicUserInfo); + userInfo = detailedUserInfo as any; + } catch (error) { + console.warn('[Auth] Failed to get detailed user info:', error); + } + + // 根据角色设置权限码 + const accessCodes = generateAccessCodes(userInfo?.roles || []); + accessStore.setAccessCodes(accessCodes); + + // 重定向到原始请求的页面或首页 + const redirect = route.query.redirect as string; + await router.push(redirect || preferences.app.defaultHomePath); + } catch (error) { + console.error('[Auth] Callback error:', error); + // 回调失败,重定向到登录页 + await router.push(LOGIN_PATH); + throw error; + } } + /** + * 检查认证状态 + * Check authentication status + */ + async function checkAuth(): Promise { + try { + const authenticated = await oidcAuthService.isAuthenticated(); + + if (authenticated) { + const user = await oidcAuthService.getUser(); + if (user) { + accessStore.setAccessToken(user.access_token); + + const tokenUserInfo = getUserInfoFromToken(user); + const basicUserInfo: any = { + userId: tokenUserInfo.id, + username: tokenUserInfo.username, + realName: tokenUserInfo.realName, + avatar: '', + token: user.access_token, + desc: tokenUserInfo.realName, + homePath: tokenUserInfo.homePath || preferences.app.defaultHomePath, + }; + userStore.setUserInfo(basicUserInfo); + + const accessCodes = generateAccessCodes(tokenUserInfo?.roles || []); + accessStore.setAccessCodes(accessCodes); + } + return true; + } + + return false; + } catch (error) { + console.error('[Auth] Check auth error:', error); + return false; + } + } + + /** + * 退出登录 + */ async function logout(redirect: boolean = true) { - const refreshToken = accessStore.refreshToken; + try { + await logoutApi(); + } catch { + // 静默处理退出错误 + } try { - if (refreshToken) { - await logoutApi(refreshToken); - } - } catch { + await oidcAuthService.signoutRedirect(); + } catch (error) { + console.error('[Auth] OIDC logout error:', error); } resetAllStores(); accessStore.setLoginExpired(false); - accessStore.setAccessToken(null); - accessStore.setRefreshToken(null); - accessStore.setExpiresAt(null); - window.location.href = getLogoutUrl(); + if (redirect) { + await router.replace(LOGIN_PATH); + } } - async function fetchUserInfo() { - const userInfo = userStore.getUserInfo; - return userInfo; + /** + * 刷新令牌 + */ + async function refreshToken(): Promise { + try { + const user = await oidcAuthService.refreshToken(); + oidcUser.value = user; + accessStore.setAccessToken(user.access_token); + + const tokenUserInfo = getUserInfoFromToken(user); + const basicUserInfo: any = { + userId: tokenUserInfo.id, + username: tokenUserInfo.username, + realName: tokenUserInfo.realName, + avatar: '', + token: user.access_token, + desc: tokenUserInfo.realName, + homePath: tokenUserInfo.homePath || preferences.app.defaultHomePath, + }; + userStore.setUserInfo(basicUserInfo); + } catch (error) { + console.error('[Auth] Refresh token error:', error); + // 刷新失败,需要重新登录 + await logout(false); + throw error; + } + } + + /** + * 生成访问权限码(简化版) + * Generate access codes (simplified version) + */ + function generateAccessCodes(roles: string[]): string[] { + // 这里可以根据角色生成权限码 + // 实际项目中应该从后端API获取 + const codes: string[] = []; + + if (roles.includes('Admin') || roles.includes('admin')) { + codes.push('user.manage', 'role.manage', 'tenant.manage', 'oauth.manage', 'log.view', 'system.config'); + } + + if (roles.length > 0) { + codes.push('user.view'); + } + + return codes; } function $reset() { loginLoading.value = false; + oidcUser.value = null; } return { $reset, authLogin, - fetchUserInfo, - handleOAuthCallback, + checkAuth, + getAccessToken, + handleCallback, loginLoading, logout, + oidcUser, + refreshToken, }; }); diff --git a/apps/web-ele/src/views/_core/authentication/callback.vue b/apps/web-ele/src/views/_core/authentication/callback.vue index c356a59..48a170f 100644 --- a/apps/web-ele/src/views/_core/authentication/callback.vue +++ b/apps/web-ele/src/views/_core/authentication/callback.vue @@ -1,58 +1,74 @@ diff --git a/apps/web-ele/src/views/_core/authentication/login.vue b/apps/web-ele/src/views/_core/authentication/login.vue index a121cdc..bbbd9f7 100644 --- a/apps/web-ele/src/views/_core/authentication/login.vue +++ b/apps/web-ele/src/views/_core/authentication/login.vue @@ -10,51 +10,72 @@ defineOptions({ name: 'Login' }); const authStore = useAuthStore(); const loading = ref(true); -const message = ref('正在跳转到认证中心...'); +const errorMessage = ref(''); onMounted(async () => { + // 自动跳转到认证中心 try { - await authStore.authLogin(); + await authStore.authLogin({}); } catch (error: any) { - message.value = error.message || '登录跳转失败'; - } finally { - setTimeout(() => { - loading.value = false; - }, 1000); + errorMessage.value = error.message || '登录跳转失败,请刷新页面重试'; + loading.value = false; } }); + +async function handleRetry() { + loading.value = true; + errorMessage.value = ''; + try { + await authStore.authLogin({}); + } catch (error: any) { + errorMessage.value = error.message || '登录跳转失败,请刷新页面重试'; + loading.value = false; + } +}