fix(auth): 修复OIDC第三方登录认证集成的潜在风险

- 实现OIDC用户自动创建和部门管理逻辑,支持并发场景下的用户去重处理
- 重构OIDC回调流程,采用一次性code机制提升安全性,避免敏感信息通过URL传递
- 增加对已注销OIDC用户的恢复支持,兼容历史后缀用户ID格式
- 优化用户软删除逻辑,使用user_id和id组合生成哈希避免重名冲突
This commit is contained in:
DSYZayn 2026-04-01 22:10:24 +08:00
parent 7f042ffb76
commit 01705aa66b
6 changed files with 275 additions and 140 deletions

View File

@ -2,7 +2,7 @@ import re
import uuid import uuid
from yuxi.utils import logger from yuxi.utils import logger
from fastapi import APIRouter, Depends, HTTPException, Request, status, UploadFile, File from fastapi import APIRouter, Body, Depends, HTTPException, Request, status, UploadFile, File
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from pydantic import BaseModel from pydantic import BaseModel
@ -30,8 +30,10 @@ from yuxi.utils.datetime_utils import utc_now_naive
from server.routers.auth_router_oidc import ( from server.routers.auth_router_oidc import (
get_oidc_config_handler, get_oidc_config_handler,
oidc_callback_handler, oidc_callback_handler,
oidc_exchange_code_handler,
oidc_login_url_handler, oidc_login_url_handler,
OIDCConfigResponse, OIDCConfigResponse,
OIDCLoginResponse,
) )
# 创建路由器 # 创建路由器
@ -681,8 +683,8 @@ async def delete_user(
# 软删除:标记删除状态并脱敏 # 软删除:标记删除状态并脱敏
import hashlib import hashlib
# 生成4位哈希基于user_id保证唯一性 # 生成4位哈希基于 user_id + id避免历史软删除记录重名冲突
hash_suffix = hashlib.sha256(user.user_id.encode()).hexdigest()[:4] hash_suffix = hashlib.sha256(f"{user.user_id}:{user.id}".encode()).hexdigest()[:4]
user.is_deleted = 1 user.is_deleted = 1
user.deleted_at = utc_now_naive() user.deleted_at = utc_now_naive()
@ -855,9 +857,16 @@ async def get_oidc_login_url(redirect_path: str = "/"):
@auth.get("/oidc/callback", response_class=RedirectResponse) @auth.get("/oidc/callback", response_class=RedirectResponse)
async def oidc_callback( async def oidc_callback(
request: Request,
code: str, code: str,
state: str, state: str,
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""处理 OIDC 回调 - 重定向到前端 Vue 路由""" """处理 OIDC 回调 - 重定向到前端 Vue 路由"""
return await oidc_callback_handler(None, code, state, db) return await oidc_callback_handler(code, state, db, request)
@auth.post("/oidc/exchange-code", response_model=OIDCLoginResponse)
async def oidc_exchange_code(code: str = Body(..., embed=True)):
"""使用一次性 code 交换 OIDC 登录数据"""
return await oidc_exchange_code_handler(code)

View File

@ -3,16 +3,16 @@
此模块包含 OIDC 认证相关的路由需要被导入到主 auth_router.py 中使用 此模块包含 OIDC 认证相关的路由需要被导入到主 auth_router.py 中使用
""" """
from urllib.parse import urlencode from urllib.parse import urlencode
from fastapi import Request import hashlib
from fastapi import HTTPException, Request, status
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from yuxi.utils import logger from yuxi.utils import logger
from yuxi.storage.postgres.models_business import User, Department from yuxi.storage.postgres.models_business import User, Department
from yuxi.repositories.user_repository import UserRepository from yuxi.repositories.user_repository import UserRepository
from yuxi.repositories.department_repository import DepartmentRepository
from server.utils.auth_utils import AuthUtils from server.utils.auth_utils import AuthUtils
from server.utils.user_utils import generate_unique_user_id
from server.utils.oidc_config import oidc_config from server.utils.oidc_config import oidc_config
from server.utils.oidc_utils import OIDCUtils from server.utils.oidc_utils import OIDCUtils
from server.utils.common_utils import log_operation from server.utils.common_utils import log_operation
@ -35,12 +35,6 @@ class OIDCConfigResponse(BaseModel):
provider_name: str | None = "OIDC登录" provider_name: str | None = "OIDC登录"
class OIDCCallbackRequest(BaseModel):
"""OIDC 回调请求"""
code: str
state: str
class OIDCLoginResponse(BaseModel): class OIDCLoginResponse(BaseModel):
"""OIDC 登录响应""" """OIDC 登录响应"""
access_token: str access_token: str
@ -67,23 +61,91 @@ async def get_or_create_oidc_department(db) -> Department | None:
dept = result.scalar_one_or_none() dept = result.scalar_one_or_none()
if not dept: if not dept:
# 创建 OIDC 用户部门 dept = Department(
dept_repo = DepartmentRepository() name=dept_name,
dept = await dept_repo.create({ description=f"{dept_name}部门",
"name": dept_name, )
"description": f"{dept_name}部门", db.add(dept)
}) try:
await db.commit()
await db.refresh(dept)
logger.info(f"Created OIDC department: {dept_name}") logger.info(f"Created OIDC department: {dept_name}")
except IntegrityError:
await db.rollback()
result = await db.execute(select(Department).filter(Department.name == dept_name))
dept = result.scalar_one_or_none()
return dept return dept
async def find_user_by_oidc_sub(db, sub: str) -> User | None: async def find_user_by_oidc_sub(db, sub: str) -> User | None:
"""通过 OIDC sub 查找用户""" """通过 OIDC sub 查找用户"""
# OIDC 用户的 user_id 格式为: oidc:{sub}
oidc_user_id = f"oidc:{sub}" oidc_user_id = f"oidc:{sub}"
# 优先匹配标准 user_idoidc:{sub}
result = await db.execute(select(User).filter(User.user_id == oidc_user_id, User.is_deleted == 0)) result = await db.execute(select(User).filter(User.user_id == oidc_user_id, User.is_deleted == 0))
return result.scalar_one_or_none() user = result.scalar_one_or_none()
if user:
return user
# 兼容历史后缀 user_idoidc:{sub}:xxxx
legacy_result = await db.execute(
select(User)
.filter(User.user_id.like(f"{oidc_user_id}:%"), User.is_deleted == 0)
.order_by(User.id.asc())
)
legacy_users = list(legacy_result.scalars().all())
if legacy_users:
if len(legacy_users) > 1:
logger.warning(f"Multiple legacy OIDC users matched for sub={sub}, use earliest id={legacy_users[0].id}")
return legacy_users[0]
return None
async def find_deleted_oidc_user_by_sub(db, sub: str) -> User | None:
"""查找已注销的 OIDC 账户(标准与历史后缀)"""
oidc_user_id = f"oidc:{sub}"
result = await db.execute(select(User).filter(User.user_id == oidc_user_id, User.is_deleted == 1))
deleted_user = result.scalar_one_or_none()
if deleted_user:
return deleted_user
legacy_result = await db.execute(
select(User)
.filter(User.user_id.like(f"{oidc_user_id}:%"), User.is_deleted == 1)
.order_by(User.id.asc())
)
return legacy_result.scalar_one_or_none()
async def build_unique_oidc_username(db, preferred_username: str, sub: str) -> str:
"""为 OIDC 用户生成不冲突的用户名"""
base_username = preferred_username.strip() if preferred_username else ""
if not base_username:
base_username = f"oidc_{sub[:8]}"
result = await db.execute(select(User.id).filter(User.username == base_username))
if result.scalar_one_or_none() is None:
return base_username
hash_suffix = hashlib.sha256(sub.encode()).hexdigest()[:6]
candidate = f"{base_username}-{hash_suffix}"
result = await db.execute(select(User.id).filter(User.username == candidate))
if result.scalar_one_or_none() is None:
return candidate
for i in range(2, 100):
indexed_candidate = f"{candidate}-{i}"
result = await db.execute(select(User.id).filter(User.username == indexed_candidate))
if result.scalar_one_or_none() is None:
return indexed_candidate
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="无法生成可用用户名,请联系管理员",
)
async def create_oidc_user(db, user_info: dict, department_id: int | None = None) -> User: async def create_oidc_user(db, user_info: dict, department_id: int | None = None) -> User:
@ -91,25 +153,19 @@ async def create_oidc_user(db, user_info: dict, department_id: int | None = None
user_repo = UserRepository() user_repo = UserRepository()
sub = user_info["sub"] sub = user_info["sub"]
username = user_info["name"] or user_info["username"] preferred_username = user_info["name"] or user_info["username"]
email = user_info["email"]
# 生成唯一的 user_id
existing_user_ids = await user_repo.get_all_user_ids()
base_username = user_info["username"]
user_id = f"oidc:{sub}" user_id = f"oidc:{sub}"
# 如果 oidc:{sub} 已存在,添加随机后缀
if user_id in existing_user_ids:
import uuid
user_id = f"oidc:{sub}:{uuid.uuid4().hex[:8]}"
# 生成随机密码OIDC 用户不需要密码登录) # 生成随机密码OIDC 用户不需要密码登录)
import secrets import secrets
random_password = secrets.token_urlsafe(32) random_password = secrets.token_urlsafe(32)
password_hash = AuthUtils.hash_password(random_password) password_hash = AuthUtils.hash_password(random_password)
# 创建用户 username = await build_unique_oidc_username(db, preferred_username, sub)
# 并发场景下兜底:若创建时发生唯一键冲突,优先复用已创建账号;否则重试用户名。
for retry_index in range(3):
try:
new_user = await user_repo.create({ new_user = await user_repo.create({
"username": username, "username": username,
"user_id": user_id, "user_id": user_id,
@ -120,9 +176,43 @@ async def create_oidc_user(db, user_info: dict, department_id: int | None = None
"department_id": department_id, "department_id": department_id,
"last_login": utc_now_naive(), "last_login": utc_now_naive(),
}) })
logger.info(f"Created OIDC user: {new_user.username} ({user_id})")
logger.info(f"Created OIDC user: {username} ({user_id})")
return new_user return new_user
except IntegrityError:
existing_user = await find_user_by_oidc_sub(db, sub)
if existing_user:
return existing_user
username = await build_unique_oidc_username(db, f"{preferred_username}-{retry_index + 2}", sub)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="创建 OIDC 用户失败,请重试",
)
async def restore_deleted_oidc_user(db, deleted_user: User, user_info: dict) -> User:
"""恢复已注销的 OIDC 用户并返回可登录用户"""
preferred_username = user_info["name"] or user_info["username"]
deleted_user.is_deleted = 0
deleted_user.deleted_at = None
deleted_user.last_login = utc_now_naive()
deleted_user.phone_number = None
deleted_user.avatar = None
# 删除流程会把用户名改成“已注销用户-xxxx”恢复时重新分配可用用户名
if deleted_user.username.startswith("已注销用户-"):
deleted_user.username = await build_unique_oidc_username(db, preferred_username, user_info["sub"])
if deleted_user.password_hash == "DELETED":
import secrets
random_password = secrets.token_urlsafe(32)
deleted_user.password_hash = AuthUtils.hash_password(random_password)
await db.commit()
await db.refresh(deleted_user)
logger.info(f"Restored deleted OIDC user: {deleted_user.username} ({deleted_user.user_id})")
return deleted_user
async def update_oidc_user_login(db, user: User) -> None: async def update_oidc_user_login(db, user: User) -> None:
@ -131,25 +221,9 @@ async def update_oidc_user_login(db, user: User) -> None:
await db.commit() await db.commit()
def _redirect_to_callback(token_data: dict) -> RedirectResponse: def _redirect_to_callback(exchange_code: str) -> RedirectResponse:
"""成功后重定向到前端 OIDC 回调页面,通过 URL 参数传递登录数据""" """成功后重定向到前端 OIDC 回调页面,仅携带一次性 code"""
params: dict = { url = f"{FRONTEND_CALLBACK_PATH}?{urlencode({'code': exchange_code})}"
"token": token_data["access_token"],
"user_id": str(token_data["user_id"]),
"username": token_data["username"],
"user_id_login": token_data["user_id_login"],
"role": token_data["role"],
}
if token_data.get("phone_number"):
params["phone_number"] = token_data["phone_number"]
if token_data.get("avatar"):
params["avatar"] = token_data["avatar"]
if token_data.get("department_id") is not None: # 0 is a valid id, so check explicitly
params["department_id"] = str(token_data["department_id"])
if token_data.get("department_name"):
params["department_name"] = token_data["department_name"]
url = f"{FRONTEND_CALLBACK_PATH}?{urlencode(params)}"
return RedirectResponse(url=url, status_code=302) return RedirectResponse(url=url, status_code=302)
@ -168,17 +242,15 @@ async def get_oidc_config_handler():
if not oidc_config.enabled or not oidc_config.is_configured(): if not oidc_config.enabled or not oidc_config.is_configured():
return OIDCConfigResponse(enabled=False) return OIDCConfigResponse(enabled=False)
login_url = await OIDCUtils.build_authorization_url()
provider_name = oidc_config.provider_name provider_name = oidc_config.provider_name
return OIDCConfigResponse(enabled=True, login_url=login_url, provider_name=provider_name) return OIDCConfigResponse(enabled=True, provider_name=provider_name)
async def oidc_callback_handler(request: Request, code: str, state: str, db): async def oidc_callback_handler(code: str, state: str, db, request: Request | None = None):
"""处理 OIDC 回调 - 重定向到前端 Vue 路由""" """处理 OIDC 回调 - 重定向到前端 Vue 路由"""
# 验证 state # 验证 state
state_data = OIDCUtils.verify_state(state) if not OIDCUtils.verify_state(state):
if not state_data:
return _redirect_to_login_with_error("登录会话已过期,请返回登录页重试") return _redirect_to_login_with_error("登录会话已过期,请返回登录页重试")
# 用授权码交换令牌 # 用授权码交换令牌
@ -210,6 +282,11 @@ async def oidc_callback_handler(request: Request, code: str, state: str, db):
await update_oidc_user_login(db, user) await update_oidc_user_login(db, user)
logger.info(f"OIDC user logged in: {user.username}") logger.info(f"OIDC user logged in: {user.username}")
elif oidc_config.auto_create_user: elif oidc_config.auto_create_user:
deleted_user = await find_deleted_oidc_user_by_sub(db, sub)
if deleted_user:
user = await restore_deleted_oidc_user(db, deleted_user, extracted_info)
logger.info(f"OIDC deleted user restored and logged in: {user.username}")
else:
# 获取或创建 OIDC 部门 # 获取或创建 OIDC 部门
dept = await get_or_create_oidc_department(db) dept = await get_or_create_oidc_department(db)
department_id = dept.id if dept else None department_id = dept.id if dept else None
@ -228,7 +305,7 @@ async def oidc_callback_handler(request: Request, code: str, state: str, db):
jwt_token = AuthUtils.create_access_token(token_data) jwt_token = AuthUtils.create_access_token(token_data)
# 记录登录操作 # 记录登录操作
await log_operation(db, user.id, "OIDC 登录") await log_operation(db, user.id, "OIDC 登录", request=request)
# 获取部门名称 # 获取部门名称
department_name = None department_name = None
@ -250,14 +327,25 @@ async def oidc_callback_handler(request: Request, code: str, state: str, db):
"department_name": department_name, "department_name": department_name,
} }
exchange_code = OIDCUtils.generate_login_code(response_data)
# 重定向到前端 OIDC 回调 Vue 页面 # 重定向到前端 OIDC 回调 Vue 页面
return _redirect_to_callback(response_data) return _redirect_to_callback(exchange_code)
async def oidc_exchange_code_handler(code: str) -> dict:
"""用一次性 code 交换登录响应数据"""
token_data = OIDCUtils.consume_login_code(code)
if not token_data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="登录 code 无效或已过期,请重新登录",
)
return token_data
async def oidc_login_url_handler(redirect_path: str = "/"): async def oidc_login_url_handler(redirect_path: str = "/"):
"""获取 OIDC 登录 URL""" """获取 OIDC 登录 URL"""
from fastapi import HTTPException, status
if not oidc_config.enabled or not oidc_config.is_configured(): if not oidc_config.enabled or not oidc_config.is_configured():
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,

View File

@ -1,5 +1,6 @@
"""OIDC 认证工具类""" """OIDC 认证工具类"""
import secrets import secrets
import time
import urllib.parse import urllib.parse
from typing import Any, Optional from typing import Any, Optional
@ -51,7 +52,24 @@ class OIDCUtils:
"""OIDC 工具类""" """OIDC 工具类"""
_metadata: Optional[OIDCProviderMetadata] = None _metadata: Optional[OIDCProviderMetadata] = None
_state_store: dict[str, dict[str, Any]] = {} # 简单的 state 存储 _state_store: dict[str, dict[str, Any]] = {}
_login_code_store: dict[str, dict[str, Any]] = {}
_state_ttl_seconds = 300
_login_code_ttl_seconds = 60
@classmethod
def _cleanup_expired_state(cls) -> None:
now = time.time()
expired = [k for k, v in cls._state_store.items() if v["expires_at"] <= now]
for key in expired:
cls._state_store.pop(key, None)
@classmethod
def _cleanup_expired_login_code(cls) -> None:
now = time.time()
expired = [k for k, v in cls._login_code_store.items() if v["expires_at"] <= now]
for key in expired:
cls._login_code_store.pop(key, None)
@classmethod @classmethod
async def get_metadata(cls) -> Optional[OIDCProviderMetadata]: async def get_metadata(cls) -> Optional[OIDCProviderMetadata]:
@ -80,14 +98,44 @@ class OIDCUtils:
@classmethod @classmethod
def generate_state(cls, redirect_path: str = "/") -> str: def generate_state(cls, redirect_path: str = "/") -> str:
"""生成 state 参数并存储""" """生成 state 参数并存储"""
cls._cleanup_expired_state()
state = secrets.token_urlsafe(32) state = secrets.token_urlsafe(32)
cls._state_store[state] = {"redirect_path": redirect_path} cls._state_store[state] = {
"redirect_path": redirect_path,
"expires_at": time.time() + cls._state_ttl_seconds,
}
return state return state
@classmethod @classmethod
def verify_state(cls, state: str) -> Optional[dict[str, Any]]: def verify_state(cls, state: str) -> Optional[dict[str, Any]]:
"""验证 state 参数""" """验证 state 参数"""
return cls._state_store.pop(state, None) state_data = cls._state_store.pop(state, None)
if not state_data:
return None
if state_data["expires_at"] <= time.time():
return None
return {"redirect_path": state_data["redirect_path"]}
@classmethod
def generate_login_code(cls, payload: dict[str, Any]) -> str:
"""生成一次性短期登录 code"""
cls._cleanup_expired_login_code()
code = secrets.token_urlsafe(32)
cls._login_code_store[code] = {
"payload": payload,
"expires_at": time.time() + cls._login_code_ttl_seconds,
}
return code
@classmethod
def consume_login_code(cls, code: str) -> Optional[dict[str, Any]]:
"""消费一次性短期登录 code"""
data = cls._login_code_store.pop(code, None)
if not data:
return None
if data["expires_at"] <= time.time():
return None
return data["payload"]
@classmethod @classmethod
def generate_nonce(cls) -> str: def generate_nonce(cls) -> str:

View File

@ -55,7 +55,7 @@
- 新增面向用户的 Langfuse 集成文档:在“智能体开发”分组中说明 Langfuse 的定位、能力、配置方式与查看路径,并与当前 `LANGFUSE_BASE_URL` 配置保持一致 - 新增面向用户的 Langfuse 集成文档:在“智能体开发”分组中说明 Langfuse 的定位、能力、配置方式与查看路径,并与当前 `LANGFUSE_BASE_URL` 配置保持一致
<!-- 添加到这里 --> <!-- 添加到这里 -->
- 新增第三方登录认证集成支持以OIDC方式接入
### 修复 ### 修复
- 优化 Agent 输入框 mention 行为:在保留附件 mention 的同时,将共享 `workspace` 文件纳入候选范围;并将 `@` 空查询时的候选列表改为空,仅在继续输入后再执行筛选,避免工作区文件过多时直接铺满下拉面板 - 优化 Agent 输入框 mention 行为:在保留附件 mention 的同时,将共享 `workspace` 文件纳入候选范围;并将 `@` 空查询时的候选列表改为空,仅在继续输入后再执行筛选,避免工作区文件过多时直接铺满下拉面板

View File

@ -2,6 +2,18 @@
* 认证相关 API * 认证相关 API
*/ */
async function parseErrorDetail(response, fallbackMessage) {
const contentType = response.headers.get('content-type') || ''
if (contentType.includes('application/json')) {
const error = await response.json()
return error?.detail || fallbackMessage
}
const text = (await response.text()).trim()
return text || fallbackMessage
}
/** /**
* 获取 OIDC 配置 * 获取 OIDC 配置
* @returns {Promise<{enabled: boolean, provider_name?: string}>} * @returns {Promise<{enabled: boolean, provider_name?: string}>}
@ -23,16 +35,15 @@ async function getOIDCLoginUrl(redirectPath = '/') {
const params = new URLSearchParams({ redirect_path: redirectPath }) const params = new URLSearchParams({ redirect_path: redirectPath })
const response = await fetch(`/api/auth/oidc/login-url?${params}`) const response = await fetch(`/api/auth/oidc/login-url?${params}`)
if (!response.ok) { if (!response.ok) {
const error = await response.json() const detail = await parseErrorDetail(response, '获取 OIDC 登录地址失败')
throw new Error(error.detail || '获取 OIDC 登录地址失败') throw new Error(detail)
} }
return response.json() return response.json()
} }
/** /**
* 处理 OIDC 回调 * 使用一次性 code 交换 OIDC 登录结果
* @param {string} code - 授权码 * @param {string} code - 一次性登录 code
* @param {string} state - state 参数
* @returns {Promise<{ * @returns {Promise<{
* access_token: string, * access_token: string,
* token_type: string, * token_type: string,
@ -46,34 +57,18 @@ async function getOIDCLoginUrl(redirectPath = '/') {
* department_name: string | null * department_name: string | null
* }>} * }>}
*/ */
async function handleOIDCCallback(code, state) { async function exchangeOIDCCode(code) {
const params = new URLSearchParams({ code, state }) const response = await fetch('/api/auth/oidc/exchange-code', {
const response = await fetch(`/api/auth/oidc/callback?${params}`)
if (!response.ok) {
const error = await response.json()
throw new Error(error.detail || 'OIDC 登录失败')
}
return response.json()
}
/**
* 执行 OIDC 登出
* @param {string} token - JWT token
* @returns {Promise<{logout_url?: string}>}
*/
async function oidcLogout(token) {
const response = await fetch('/api/auth/oidc/logout', {
method: 'POST', method: 'POST',
headers: { headers: {
'Authorization': `Bearer ${token}` 'Content-Type': 'application/json'
} },
body: JSON.stringify({ code })
}) })
if (!response.ok) { if (!response.ok) {
const error = await response.json() const detail = await parseErrorDetail(response, 'OIDC 登录失败')
throw new Error(error.detail || 'OIDC 登出失败') throw new Error(detail)
} }
return response.json() return response.json()
@ -82,6 +77,5 @@ async function oidcLogout(token) {
export const authApi = { export const authApi = {
getOIDCConfig, getOIDCConfig,
getOIDCLoginUrl, getOIDCLoginUrl,
handleOIDCCallback, exchangeOIDCCode,
oidcLogout,
} }

View File

@ -32,6 +32,7 @@ import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { useAgentStore } from '@/stores/agent' import { useAgentStore } from '@/stores/agent'
import { authApi } from '@/apis/auth_api'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
const router = useRouter() const router = useRouter()
@ -50,42 +51,37 @@ const goToLogin = () => {
router.push('/login') router.push('/login')
} }
// OIDC - URL token // OIDC - URL code
const handleCallback = () => { const handleCallback = async () => {
try { try {
// URL token const code = route.query.code
const token = route.query.token
const userId = route.query.user_id
const username = route.query.username
const userIdLogin = route.query.user_id_login
const phoneNumber = route.query.phone_number
const avatar = route.query.avatar
const role = route.query.role
const departmentId = route.query.department_id
const departmentName = route.query.department_name
// //
if (!token || !userId || !username) { if (!code || typeof code !== 'string') {
loading.value = false loading.value = false
error.value = true error.value = true
errorTitle.value = '参数错误' errorTitle.value = '参数错误'
errorMessage.value = '缺少必要的登录信息,请重新登录' errorMessage.value = '缺少有效的登录 code请重新登录'
return return
} }
const tokenData = await authApi.exchangeOIDCCode(code)
await router.replace({ path: route.path, query: {} })
// //
userStore.token = token userStore.token = tokenData.access_token
userStore.userId = parseInt(userId) userStore.userId = tokenData.user_id
userStore.username = username userStore.username = tokenData.username
userStore.userIdLogin = userIdLogin || '' userStore.userIdLogin = tokenData.user_id_login || ''
userStore.phoneNumber = phoneNumber || '' userStore.phoneNumber = tokenData.phone_number || ''
userStore.avatar = avatar || '' userStore.avatar = tokenData.avatar || ''
userStore.userRole = role || 'user' userStore.userRole = tokenData.role || 'user'
userStore.departmentId = departmentId ? parseInt(departmentId) : null userStore.departmentId = tokenData.department_id || null
userStore.departmentName = departmentName || '' userStore.departmentName = tokenData.department_name || ''
// token localStorage // token localStorage
localStorage.setItem('user_token', token) localStorage.setItem('user_token', tokenData.access_token)
// //
message.success('登录成功') message.success('登录成功')
@ -117,19 +113,19 @@ const handleCallback = () => {
loading.value = false loading.value = false
error.value = true error.value = true
errorTitle.value = '登录失败' errorTitle.value = '登录失败'
errorMessage.value = err.message || '处理登录请求时发生错误,请重试' errorMessage.value = err?.message || '处理登录请求时发生错误,请重试'
} }
} }
// //
onMounted(() => { onMounted(async () => {
// //
if (userStore.isLoggedIn) { if (userStore.isLoggedIn) {
router.push('/') router.push('/')
return return
} }
handleCallback() await handleCallback()
}) })
</script> </script>