diff --git a/docs/OIDC认证调用流程.md b/docs/OIDC认证调用流程.md index f26aaaa1..849b9c5c 100644 --- a/docs/OIDC认证调用流程.md +++ b/docs/OIDC认证调用流程.md @@ -34,9 +34,9 @@ - 若用户不存在,则自动创建本地账号和默认租户; - 最终签发 WeKnora 自己的本地 JWT(`token` / `refresh_token`)。 4. 后端不会直接把登录结果放在 query string 中,而是: - - 将结果 JSON 序列化后再做 base64url 编码; + - 将登录结果 JSON( `success`、`token`、`refresh_token` 等字段)序列化后再做 base64url 编码; - 以 `#oidc_result=...` 的形式重定向回前端; - - 前端在 `App.vue` 中统一解析 hash,完成登录态持久化。 + - 前端在 `App.vue` 中统一解析 hash,再调用 `/api/v1/auth/me` 补全用户和租户信息。 因此,**OIDC Provider 的 token 只用于后端换取用户身份,本项目真正的业务访问凭证仍然是 WeKnora 自己签发的 JWT**。 @@ -93,7 +93,9 @@ sequenceDiagram BE-->>FE: 302 到 /#oidc_result=... FE->>FE: App.vue 解析 hash - FE->>FE: 写入 authStore/token/tenant + FE->>BE: GET /api/v1/auth/me + BE-->>FE: { user, tenant } + FE->>FE: 写入 authStore/token/user/tenant FE-->>U: 跳转 /platform/knowledge-bases ``` @@ -120,7 +122,7 @@ flowchart TD M -- 是 --> O[使用现有用户] N --> P[签发 WeKnora JWT] O --> P - P --> Q[编码为 oidc_result] + P --> Q[编码最小必要登录结果为 oidc_result] Q --> R[302 重定向到前端首页 hash] ``` @@ -422,12 +424,12 @@ GenerateTokens(ctx, user) 并写入本地 `auth_tokens` 存储(通过 `tokenRepo.CreateToken`)。 -最终后端还会查询当前用户所属租户,并返回: +最终后端会返回 OIDC 回调载荷,其中包含: -- `user` -- `tenant` - `token` - `refresh_token` +- `success` +- `message` - `is_new_user` 这一步意味着: @@ -502,10 +504,10 @@ window.location.hash 1. base64url 解码并反序列化; 2. 如果 `response.success=true`: - - 写入 `authStore.setUser(...)` - - 写入 `authStore.setToken(...)` - - 写入 `authStore.setRefreshToken(...)` - - 写入 `authStore.setTenant(...)` + - 先写入 `authStore.setToken(...)` + - 再写入 `authStore.setRefreshToken(...)` + - 随后调用 `/api/v1/auth/me` + - 用 `/auth/me` 返回的 `user` / `tenant` 执行 `authStore.setUser(...)` 与 `authStore.setTenant(...)` 3. 最终跳转到: ```text diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 9421b1c9..d04429fa 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n' import { MessagePlugin } from 'tdesign-vue-next' import ManualKnowledgeEditor from '@/components/manual-knowledge-editor.vue' import { useAuthStore } from '@/stores/auth' +import { getCurrentUser } from '@/api/auth' // TDesign locale configs import enUSConfig from 'tdesign-vue-next/esm/locale/en_US' @@ -37,32 +38,50 @@ const clearOIDCCallbackState = (path = '/') => { window.history.replaceState({}, document.title, path) } -const persistOIDCLoginResponse = async (response: any) => { - if (response.user && response.tenant && response.token) { - authStore.setUser({ - id: response.user.id || '', - username: response.user.username || '', - email: response.user.email || '', - avatar: response.user.avatar, - tenant_id: String(response.tenant.id) || '', - can_access_all_tenants: response.user.can_access_all_tenants || false, - created_at: response.user.created_at || new Date().toISOString(), - updated_at: response.user.updated_at || new Date().toISOString() - }) - authStore.setToken(response.token) - if (response.refresh_token) { - authStore.setRefreshToken(response.refresh_token) - } - authStore.setTenant({ - id: String(response.tenant.id) || '', - name: response.tenant.name || '', - api_key: response.tenant.api_key || '', - owner_id: response.user.id || '', - created_at: response.tenant.created_at || new Date().toISOString(), - updated_at: response.tenant.updated_at || new Date().toISOString() - }) +const syncOIDCUserContext = async () => { + const currentUserResponse = await getCurrentUser() + if (!currentUserResponse.success || !currentUserResponse.data?.user || !currentUserResponse.data?.tenant) { + throw new Error(currentUserResponse.message || 'Failed to get user information') } + const { user, tenant } = currentUserResponse.data + authStore.setUser({ + id: user.id || '', + username: user.username || '', + email: user.email || '', + avatar: user.avatar, + tenant_id: String(user.tenant_id || tenant.id || ''), + can_access_all_tenants: user.can_access_all_tenants || false, + created_at: user.created_at || new Date().toISOString(), + updated_at: user.updated_at || new Date().toISOString() + }) + authStore.setTenant({ + id: String(tenant.id) || '', + name: tenant.name || '', + api_key: tenant.api_key || '', + owner_id: tenant.owner_id || user.id || '', + description: tenant.description, + status: tenant.status, + business: tenant.business, + storage_quota: tenant.storage_quota, + storage_used: tenant.storage_used, + created_at: tenant.created_at || new Date().toISOString(), + updated_at: tenant.updated_at || new Date().toISOString() + }) +} + +const persistOIDCLoginResponse = async (response: any) => { + if (!response.token) { + throw new Error(response.message || 'OIDC login failed') + } + + authStore.setToken(response.token) + if (response.refresh_token) { + authStore.setRefreshToken(response.refresh_token) + } + + await syncOIDCUserContext() + await nextTick() router.replace('/platform/knowledge-bases') } @@ -106,6 +125,7 @@ const handleGlobalOIDCCallback = async () => { MessagePlugin.error(response.message || 'OIDC login failed') } catch (error: any) { console.error('Global OIDC callback handling failed:', error) + authStore.logout() clearOIDCCallbackState('/login') await router.replace('/login') MessagePlugin.error(error.message || 'OIDC login failed') diff --git a/internal/application/service/user.go b/internal/application/service/user.go index d5e1700a..79616cd6 100644 --- a/internal/application/service/user.go +++ b/internal/application/service/user.go @@ -306,16 +306,9 @@ func (s *userService) LoginWithOIDC(ctx context.Context, code, redirectURI strin return nil, fmt.Errorf("failed to generate local tokens: %w", err) } - tenant, err := s.tenantService.GetTenantByID(ctx, user.TenantID) - if err != nil { - logger.Warnf(ctx, "Failed to get tenant info after OIDC login: %v", err) - } - return &types.OIDCCallbackResponse{ Success: true, - Message: "Login successful", - User: user, - Tenant: tenant, + Message: "登录成功", Token: accessToken, RefreshToken: refreshToken, IsNewUser: isNewUser,