From 22f1a243a0c12075abdf75fea3383da586c3fde9 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 14 Mar 2026 18:35:42 +0800 Subject: [PATCH] Update tenant id in ctx --- backend/app/admin/api/v1/monitor/online.py | 2 +- backend/app/admin/schema/user.py | 11 +++--- backend/app/admin/service/auth_service.py | 14 ++++--- backend/common/dataclasses.py | 6 +-- backend/common/security/jwt.py | 44 +++++++++++----------- backend/middleware/jwt_auth_middleware.py | 5 --- backend/middleware/state_middleware.py | 2 +- 7 files changed, 41 insertions(+), 43 deletions(-) diff --git a/backend/app/admin/api/v1/monitor/online.py b/backend/app/admin/api/v1/monitor/online.py index 5edfe5bc..c021c2c3 100644 --- a/backend/app/admin/api/v1/monitor/online.py +++ b/backend/app/admin/api/v1/monitor/online.py @@ -40,7 +40,7 @@ async def get_sessions( for key in token_keys: token = await redis_client.get(key) token_payload = jwt_decode(token) - user_id = token_payload.id + user_id = token_payload.user_id session_uuid = token_payload.session_uuid token_detail = GetTokenDetail( id=user_id, diff --git a/backend/app/admin/schema/user.py b/backend/app/admin/schema/user.py index 2a783305..960cc3d4 100644 --- a/backend/app/admin/schema/user.py +++ b/backend/app/admin/schema/user.py @@ -20,11 +20,11 @@ class AuthSchemaBase(SchemaBase): class AuthLoginParam(AuthSchemaBase): """用户登录参数""" + if settings.TENANT_ENABLED: + tenant_id: int = Field(settings.TENANT_DEFAULT_ID, description='租户 ID') uuid: str | None = Field(None, description='验证码 UUID') captcha: str | None = Field(None, description='验证码') - if settings.TENANT_ENABLED: - tenant_id: int = Field(settings.TENANT_DEFAULT_ID, description='租户 ID') class AddUserParam(AuthSchemaBase): @@ -83,6 +83,10 @@ class GetUserInfoDetail(UserInfoSchemaBase): model_config = ConfigDict(from_attributes=True) + if settings.TENANT_ENABLED: + tenant_id: int = Field(description='租户 ID') + + dept_id: int | None = Field(None, description='部门 ID') id: int = Field(description='用户 ID') uuid: str = Field(description='用户 UUID') status: StatusType = Field(description='状态') @@ -91,9 +95,6 @@ class GetUserInfoDetail(UserInfoSchemaBase): is_multi_login: bool = Field(description='是否允许多端登录') join_time: datetime = Field(description='加入时间') last_login_time: datetime | None = Field(None, description='最后登录时间') - dept_id: int | None = Field(None, description='部门 ID') - if settings.TENANT_ENABLED: - tenant_id: int = Field(description='租户 ID') class GetUserInfoWithRelationDetail(GetUserInfoDetail): diff --git a/backend/app/admin/service/auth_service.py b/backend/app/admin/service/auth_service.py index 7f8f70c7..b66fe391 100644 --- a/backend/app/admin/service/auth_service.py +++ b/backend/app/admin/service/auth_service.py @@ -101,6 +101,7 @@ class AuthService: """ user = None tenant_id = settings.TENANT_DEFAULT_ID + try: await load_login_config(db) if settings.LOGIN_CAPTCHA_ENABLED: @@ -116,13 +117,16 @@ class AuthService: if settings.TENANT_ENABLED: tenant_id = obj.tenant_id await check_tenant_status(db, tenant_id) - ctx.tenant_id = tenant_id # 用于操作日志 + + # 登录前先写入当前租户,供后续登录请求流程使用 + ctx.tenant_id = tenant_id + user, days_remaining = await self.user_verify(db, obj.username, obj.password) await user_dao.update_login_time(db, obj.username) await db.refresh(user) access_token_data = await create_access_token( user.id, - getattr(user, 'tenant_id', tenant_id), + user.tenant_id, multi_login=user.is_multi_login, # extra info username=user.username, @@ -222,7 +226,7 @@ class AuthService: raise errors.RequestError(msg='Refresh Token 已过期,请重新登录') token_payload = jwt_decode(refresh_token) - user = await user_dao.get(db, token_payload.id) + user = await user_dao.get(db, token_payload.user_id) if not user: raise errors.NotFoundError(msg='用户不存在') if not user.status: @@ -233,7 +237,7 @@ class AuthService: refresh_token, token_payload.session_uuid, user.id, - getattr(user, 'tenant_id', token_payload.tenant_id), + user.tenant_id, multi_login=user.is_multi_login, # extra info username=user.username, @@ -263,7 +267,7 @@ class AuthService: try: token = get_token(request) token_payload = jwt_decode(token) - user_id = token_payload.id + user_id = token_payload.user_id session_uuid = token_payload.session_uuid refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY) except errors.TokenError: diff --git a/backend/common/dataclasses.py b/backend/common/dataclasses.py index f8474354..341e0b56 100644 --- a/backend/common/dataclasses.py +++ b/backend/common/dataclasses.py @@ -56,12 +56,10 @@ class NewToken: @dataclasses.dataclass class TokenPayload: - """JWT 载荷,tenant_id 为多租户鉴权必填字段。""" - - id: int + user_id: int + tenant_id: int session_uuid: str expire_time: datetime - tenant_id: int @dataclasses.dataclass diff --git a/backend/common/security/jwt.py b/backend/common/security/jwt.py index dea98ef5..1941e7c7 100644 --- a/backend/common/security/jwt.py +++ b/backend/common/security/jwt.py @@ -53,14 +53,14 @@ def jwt_decode(token: str) -> TokenPayload: user_id = payload.get('sub') expire = payload.get('exp') tenant_id = payload.get('tenant_id') - if not session_uuid or not user_id or not expire or not tenant_id: + if not session_uuid or not user_id or not expire or tenant_id is None: raise errors.TokenError(msg='Token 无效') except ExpiredSignatureError: raise errors.TokenError(msg='Token 已过期') except (JWTError, Exception): raise errors.TokenError(msg='Token 无效') return TokenPayload( - id=int(user_id), + user_id=int(user_id), session_uuid=session_uuid, expire_time=timezone.from_datetime(timezone.to_utc(expire)), tenant_id=int(tenant_id), @@ -298,6 +298,26 @@ async def get_jwt_user(user_id: int) -> GetUserInfoWithRelationDetail: return user +async def jwt_authentication(token: str) -> GetUserInfoWithRelationDetail: + """ + JWT 认证 + + :param token: JWT token + :return: + """ + token_payload = jwt_decode(token) + ctx.user_id = token_payload.user_id + ctx.tenant_id = token_payload.tenant_id + redis_token = await redis_client.get(f'{settings.TOKEN_REDIS_PREFIX}:{ctx.user_id}:{token_payload.session_uuid}') + if not redis_token: + raise errors.TokenError(msg='Token 已过期') + + if token != redis_token: + raise errors.TokenError(msg='Token 已失效') + + return await get_jwt_user(ctx.user_id) + + def superuser_verify(request: Request, _token: str = DependsJwtAuth) -> bool: """ 验证当前用户超级管理员权限 @@ -312,25 +332,5 @@ def superuser_verify(request: Request, _token: str = DependsJwtAuth) -> bool: return superuser -async def jwt_authentication(token: str) -> GetUserInfoWithRelationDetail: - """ - JWT 认证 - - :param token: JWT token - :return: - """ - token_payload = jwt_decode(token) - ctx.tenant_id = token_payload.tenant_id - user_id = token_payload.id - redis_token = await redis_client.get(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{token_payload.session_uuid}') - if not redis_token: - raise errors.TokenError(msg='Token 已过期') - - if token != redis_token: - raise errors.TokenError(msg='Token 已失效') - - return await get_jwt_user(user_id) - - # 超级管理员鉴权依赖注入 DependsSuperUser = Depends(superuser_verify) diff --git a/backend/middleware/jwt_auth_middleware.py b/backend/middleware/jwt_auth_middleware.py index 3b35a639..8ed032e9 100644 --- a/backend/middleware/jwt_auth_middleware.py +++ b/backend/middleware/jwt_auth_middleware.py @@ -7,7 +7,6 @@ from starlette.authentication import AuthenticationError as StarletteAuthenticat from starlette.requests import HTTPConnection from backend.app.admin.schema.user import GetUserInfoWithRelationDetail -from backend.common.context import ctx from backend.common.exception.errors import TokenError from backend.common.log import log from backend.common.security.jwt import jwt_authentication @@ -96,10 +95,6 @@ class JwtAuthMiddleware(AuthenticationBackend): log.exception(f'JWT 授权异常:{e}') raise AuthenticationError(code=getattr(e, 'code', 500), msg=getattr(e, 'msg', 'Internal Server Error')) - # 设置用户 ID 和租户 ID 到上下文 - ctx.user_id = user.id - ctx.tenant_id = getattr(user, 'tenant_id', settings.TENANT_DEFAULT_ID) - # 请注意,此返回使用非标准模式,所以在认证通过时,将丢失某些标准特性 # 标准返回模式请查看:https://www.starlette.io/authentication/ return AuthCredentials(['authenticated']), user diff --git a/backend/middleware/state_middleware.py b/backend/middleware/state_middleware.py index 0314e3aa..4d400051 100644 --- a/backend/middleware/state_middleware.py +++ b/backend/middleware/state_middleware.py @@ -29,7 +29,7 @@ class StateMiddleware(BaseHTTPMiddleware): ctx.browser = ua_info.browser ctx.device = ua_info.device - # 为非授权接口设置默认租户 ID + # 为每个请求上下文注入默认租户 ID,授权接口认证成功后会覆盖为真实值 ctx.tenant_id = settings.TENANT_DEFAULT_ID response = await call_next(request)