docs/OIDC认证调用流程.md
本文档说明 WeKnora 当前 OIDC 登录能力的实际调用过程,覆盖:
本文内容基于当前项目实现,相关代码主要位于:
internal/router/router.gointernal/handler/auth.gointernal/application/service/user.gointernal/config/config.gofrontend/src/views/auth/Login.vuefrontend/src/App.vuefrontend/src/api/auth/index.tsmisc/dex-config.yaml本项目的 OIDC 登录采用的是 后端发起授权参数生成、后端接收回调并完成 code 换 token、前端通过 URL hash 接收最终登录结果 的模式。
和常见的纯前端 OIDC SDK 不同,WeKnora 的特点是:
code 向 OIDC Provider 换取 token。token / refresh_token)。success、token、refresh_token 等字段)序列化后再做 base64url 编码;#oidc_result=... 的形式重定向回前端;App.vue 中统一解析 hash,再调用 /api/v1/auth/me 补全用户和租户信息。因此,OIDC Provider 的 token 只用于后端换取用户身份,本项目真正的业务访问凭证仍然是 WeKnora 自己签发的 JWT。
当前 OIDC 相关接口均注册在 internal/router/router.go 中:
GET /api/v1/auth/oidc/config
GET /api/v1/auth/oidc/url
GET /api/v1/auth/oidc/callback
其中,前端常规登录仍然使用:
POST /api/v1/auth/loginPOST /api/v1/auth/refreshPOST /api/v1/auth/logoutGET /api/v1/auth/mesequenceDiagram
autonumber
participant U as 用户浏览器
participant FE as 前端(Login/App)
participant BE as WeKnora 后端
participant OP as OIDC Provider
FE->>BE: GET /api/v1/auth/oidc/config
BE-->>FE: { enabled, provider_display_name }
U->>FE: 点击“OIDC 登录”
FE->>BE: GET /api/v1/auth/oidc/url?redirect_uri=...
BE-->>FE: { success, authorization_url, state }
FE->>OP: 浏览器跳转到 authorization_url
OP-->>BE: GET /api/v1/auth/oidc/callback?code=...&state=...
BE->>OP: POST token endpoint (code 换 token)
OP-->>BE: access_token / id_token
BE->>OP: GET userinfo endpoint(可选)
OP-->>BE: 用户信息 claims
BE->>BE: 查找/自动创建本地用户
BE->>BE: 签发本地 token、refresh_token
BE-->>FE: 302 到 /#oidc_result=...
FE->>FE: App.vue 解析 hash
FE->>BE: GET /api/v1/auth/me
BE-->>FE: { user, tenant }
FE->>FE: 写入 authStore/token/user/tenant
FE-->>U: 跳转 /platform/knowledge-bases
flowchart TD
A[OIDC Provider 回调到 /api/v1/auth/oidc/callback] --> B{query 中是否有 error}
B -- 是 --> C[拼接 #oidc_error 和 error_description]
C --> D[302 重定向到前端登录页]
B -- 否 --> E[解析 state]
E --> F{state 是否合法且包含 redirect_uri}
F -- 否 --> G[302 到前端并带 #oidc_error=invalid_state]
F -- 是 --> H{是否有 code}
H -- 否 --> I[302 到前端并带 #oidc_error=missing_code]
H -- 是 --> J[调用 LoginWithOIDC,传入 code 和 redirect_uri]
J --> K[向 OIDC token endpoint 换 token]
K --> L[解析 id_token / 调用 userinfo]
L --> M{本地用户是否存在}
M -- 否 --> N[自动注册新用户并创建默认租户]
M -- 是 --> O[使用现有用户]
N --> P[签发 WeKnora JWT]
O --> P
P --> Q[编码最小必要登录结果为 oidc_result]
Q --> R[302 重定向到前端首页 hash]
登录页组件位于 frontend/src/views/auth/Login.vue。
页面加载时会执行:
loadOIDCConfig()
该函数调用:
getOIDCConfig() -> GET /api/v1/auth/oidc/config
后端 GetOIDCConfig 会读取 configInfo.OIDCAuth:
enabled: 是否启用 OIDCprovider_display_name: 登录按钮上展示的供应商名称前端据此决定:
用户点击按钮后,Login.vue 中会执行 handleOIDCLogin()。
核心逻辑:
const getBackendOIDCRedirectURI = () => `${window.location.origin}/api/v1/auth/oidc/callback`
其中:
redirect_uri:提供给 OIDC Provider 的回调地址,必须是后端地址。登录成功后,后端固定回跳前端首页 /,并通过 hash 传递 OIDC 结果。
GET /api/v1/auth/oidc/url?redirect_uri=...
authorization_url 后,前端直接执行:window.location.href = authorizationURL
浏览器随后离开 WeKnora 页面,跳转到 OIDC Provider 的授权页。
对应处理器:AuthHandler.GetOIDCAuthorizationURL
对应服务:userService.GetOIDCAuthorizationURL
后端要求以下参数必须存在:
redirect_uri否则直接返回校验错误。
getOIDCConfig() 会执行以下逻辑:
OIDCAuth.Enable 是否为 true;ProviderDisplayName 默认是 OIDCScopes 默认是 openid profile emailUserInfoMapping.Username 默认是 nameUserInfoMapping.Email 默认是 emaildiscovery_url 拉取 OIDC Discovery 文档;authorization_endpointtoken_endpointuserinfo_endpoint后端不会把 state 只当成随机串,而是编码了一个 JSON 结构:
{
"nonce": "随机字符串",
"redirect_uri": "后端回调地址"
}
然后再做 base64url 编码,作为 state 传给 Provider。
这样在 OIDC Provider 回调时,后端就可以从 state 里还原:
redirect_uri后端最终拼接的参数包含:
response_type=codeclient_idredirect_uriscopestate然后返回给前端:
{
"success": true,
"provider_display_name": "Dex",
"authorization_url": "...",
"state": "..."
}
OIDC Provider 完成认证后,会回调:
GET /api/v1/auth/oidc/callback
处理器为 AuthHandler.OIDCRedirectCallback。
如果 query 中带有:
errorerror_description后端不会返回 JSON,而是直接 302 到前端首页 /,并带上 hash:
#oidc_error=...&oidc_error_description=...
后端会把 state 做 base64url 解码并解析为结构体。
如果出现以下情况,将判定失败:
state 无法解码state.redirect_uri 为空失败时会重定向到前端首页:
#oidc_error=invalid_state
如果没有收到 code,则重定向:
#oidc_error=missing_code
若 state 与 code 都合法,则调用:
LoginWithOIDC(ctx, code, decodedState.RedirectURI)
注意这里传入的是 state 中保存的 redirect_uri,而不是重新拼接的地址,这样保证了 code 交换时使用的 redirect_uri 和授权时完全一致。
核心逻辑位于 internal/application/service/user.go。
exchangeOIDCCode() 会向 OIDC Provider 的 token_endpoint 发起:
POST application/x-www-form-urlencoded
表单参数包括:
grant_type=authorization_codecoderedirect_uriclient_idclient_secret期望返回字段:
access_tokenid_tokentoken_type如果 access_token 和 id_token 都缺失,则认为失败。
resolveOIDCUserInfo() 的处理顺序是:
id_token,先本地解码 JWT payload,提取 claims;userinfo_endpoint 且有 access_token,再调用 userinfo 接口;user_info_mapping 提取:
默认映射:
username -> nameemail -> email另外还有回退逻辑:
preferred_usernamename如果最终没有拿到邮箱,则直接报错,因为本地用户是按 email 关联的。
后端使用 OIDC 返回的邮箱执行:
userRepo.GetUserByEmail(ctx, userInfo.Email)
若本地不存在该邮箱用户,则调用 provisionOIDCUser() 自动创建账号。
自动创建逻辑包括:
-1、-2 等后缀;Register() 流程创建用户;Register() 内部还会自动创建默认租户。因此,首次使用 OIDC 登录的用户,不需要提前在 WeKnora 中手工建号。
如果找到的本地用户 IsActive=false,则登录失败,回调给前端的错误信息为:
Account is disabled
OIDC 登录成功后,后端不会直接把 OIDC token 交给前端使用,而是继续执行:
GenerateTokens(ctx, user)
生成两类 JWT:
token:访问令牌,默认 24 小时refresh_token:刷新令牌,默认 7 天并写入本地 auth_tokens 存储(通过 tokenRepo.CreateToken)。
最终后端会返回 OIDC 回调载荷,其中包含:
tokenrefresh_tokensuccessmessageis_new_user这一步意味着:
OIDC 只负责“确认你是谁”,WeKnora 自己负责“签发系统内可用的业务令牌”。
OIDCRedirectCallback 在拿到 OIDCCallbackResponse 后,会执行:
/#oidc_result=ENCODED_PAYLOAD
失败时则返回:
/#oidc_error=...&oidc_error_description=...
这里使用 hash 的好处是:
前端不是在 Login.vue 中处理回调,而是在 frontend/src/App.vue 中统一处理。
这样即使后端把用户重定向到 /,应用根组件也能接住这次 OIDC 登录结果。
应用挂载时执行:
handleGlobalOIDCCallback()
它会读取:
window.location.hash
并解析以下字段:
oidc_erroroidc_error_descriptionoidc_result如果存在 oidc_error:
clearOIDCCallbackState('/login') 清理 URL;/login;如果存在 oidc_result:
response.success=true:
authStore.setToken(...)authStore.setRefreshToken(...)/api/v1/auth/me/auth/me 返回的 user / tenant 执行 authStore.setUser(...) 与 authStore.setTenant(...)/platform/knowledge-bases
这与普通账号密码登录成功后的持久化逻辑保持一致。
OIDC 配置定义位于 internal/config/config.go,环境变量示例见 .env.example。
| 配置项 | 说明 |
|---|---|
OIDC_AUTH_ENABLE | 是否启用 OIDC 登录 |
OIDC_AUTH_ISSUER_URL | Issuer 地址,可用于自动拼 discovery URL |
OIDC_AUTH_DISCOVERY_URL | OIDC Discovery 地址 |
OIDC_AUTH_PROVIDER_DISPLAY_NAME | 前端按钮显示名称 |
OIDC_AUTH_CLIENT_ID | OIDC Client ID |
OIDC_AUTH_CLIENT_SECRET | OIDC Client Secret |
OIDC_AUTH_AUTHORIZATION_ENDPOINT | 授权端点,可选 |
OIDC_AUTH_TOKEN_ENDPOINT | Token 端点,可选 |
OIDC_AUTH_USER_INFO_ENDPOINT | UserInfo 端点,可选 |
OIDC_AUTH_SCOPES | Scope 列表,默认 openid profile email |
OIDC_USER_INFO_MAPPING_USER_NAME | claims 中映射到用户名的字段名 |
OIDC_USER_INFO_MAPPING_EMAIL | claims 中映射到邮箱的字段名 |
当 OIDC_AUTH_ENABLE=true 时,后端校验要求:
client_idclient_secretdiscovery_urlauthorization_endpoint + token_endpointDex 是一个简单易用的OIDC Provider,您可以通过它对接多种第三方认证系统(如OAuth2.0,Google,GitHub,LDAP等)。除了Dex之外,您也可以选择KeyCloak等其他符合OpenID Connect协议的Provider进行接入。
项目中已提供 Dex 示例配置:misc/dex-config.yaml。
其中静态客户端配置示例:
staticClients:
- id: weknora
redirectURIs:
- 'http://127.0.0.1:5173/api/v1/auth/oidc/callback'
- 'http://127.0.0.1/api/v1/auth/oidc/callback'
name: 'WeKnora'
# secret: <YOUR_SECRET_HERE>
这说明本地调试时,需要确保 Provider 注册的 redirect URI 与前端实际传给后端的 redirect_uri 完全一致。
前端当前实现中使用的是:
${window.location.origin}/api/v1/auth/oidc/callback
所以:
http://127.0.0.1:5173 访问,则 redirect URI 为
http://127.0.0.1:5173/api/v1/auth/oidc/callbackhttp://127.0.0.1/api/v1/auth/oidc/callbackProvider 必须提前把这些地址加入白名单。
可以把当前 OIDC 登录理解为以下 4 个阶段:
前端调用 /auth/oidc/config,决定是否展示第三方登录入口。
前端调用 /auth/oidc/url 获取授权地址,然后跳转到 OIDC Provider。
Provider 回调后端 /auth/oidc/callback,后端用 code 换 token、拉取用户信息、关联或创建本地用户,并签发 WeKnora JWT。
后端 302 回前端,并通过 #oidc_result 传递登录结果;前端在 App.vue 中统一解析,写入本地登录态并进入业务页面。
redirect_uri 必须严格匹配 Provider 客户端配置。/api/v1/auth/oidc/callback
/state 做了编码封装,但 没有服务端持久化 state/nonce 校验;它主要用于传递上下文和基本防错,而不是完整的防重放机制。frontend/src/views/auth/Login.vuefrontend/src/api/auth/index.tsfrontend/src/views/auth/Login.vuefrontend/src/App.vueinternal/router/router.gointernal/handler/auth.gointernal/application/service/user.gointernal/config/config.gomisc/dex-config.yaml