132 lines
5.4 KiB
Python
132 lines
5.4 KiB
Python
"""项目自定义认证后端
|
||
|
||
SimpleJWT 的 JWTAuthentication 在 __init__ 中调用 django.contrib.auth.get_user_model(),
|
||
返回 AUTH_USER_MODEL 指定的模型(未配置时为 Django 默认 auth.User)。
|
||
|
||
本项目使用 gvsds.apps.core.models.User 作为用户模型(未设置 AUTH_USER_MODEL),
|
||
因此需要覆盖 user_model,使 JWT 认证查询正确的用户表。
|
||
|
||
同时支持从 Authorization 请求头和 HttpOnly Cookie 读取 JWT,
|
||
以兼容前端基于 Cookie 的认证方式(credentials: 'include')。
|
||
|
||
额外职责:JWT 认证成功后立即解析当前角色上下文(current_role / current_company_uuid /
|
||
current_tenant_uuid),填充到 request 对象。这解决了 Django 中间件在 DRF 认证之前运行、
|
||
导致中间件无法获取已认证用户的问题。
|
||
"""
|
||
|
||
import logging
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||
|
||
from gvsds.apps.core.models import User
|
||
|
||
_logger = logging.getLogger(__name__)
|
||
|
||
#: 存储 Access Token 的 Cookie 名称
|
||
JWT_ACCESS_COOKIE = 'AccessToken'
|
||
#: 存储 Refresh Token 的 Cookie 名称
|
||
JWT_REFRESH_COOKIE = 'RefreshToken'
|
||
|
||
|
||
def ResolveRoleContext(request, user):
|
||
"""解析当前角色上下文,填充 request.current_role / current_company_uuid / current_tenant_uuid
|
||
|
||
从 CurrentRoleUUID Cookie 读取角色 UUID,查询 UserRole + Role,
|
||
设置 request.current_role / current_company_uuid / current_tenant_uuid。
|
||
|
||
Args:
|
||
request: HTTP 请求对象(会被原地修改)
|
||
user: 已认证的 User 实例
|
||
"""
|
||
# 确保默认值(防止重复调用时残留旧值)
|
||
request.current_role = None
|
||
request.current_company_uuid = None
|
||
request.current_tenant_uuid = None
|
||
|
||
if not user or not getattr(user, 'is_authenticated', False):
|
||
if hasattr(user, 'IsAuthenticated'):
|
||
if not user.IsAuthenticated:
|
||
return
|
||
else:
|
||
return
|
||
|
||
from gvsdsdk.models import UserRole, Role
|
||
from gvsdsdk.model_utils import UUIDObj
|
||
|
||
role_uuid_str = request.COOKIES.get('CurrentRoleUUID', '')
|
||
if not role_uuid_str:
|
||
_logger.warning('[ResolveRoleContext] 已认证用户 %s 无 CurrentRoleUUID cookie', user)
|
||
return
|
||
|
||
try:
|
||
role_uuid_bytes = UUIDObj(role_uuid_str).bytes
|
||
user_role = UserRole.objects.filter(
|
||
UserUUID=user.UserUUID,
|
||
RoleUUID=role_uuid_bytes
|
||
).first()
|
||
if not user_role:
|
||
_logger.warning('[ResolveRoleContext] cookie=%s 但 UserRole 未找到 (user=%s)',
|
||
role_uuid_str[:36], user)
|
||
return
|
||
# 单独查询 Role 实例(Role.TenantUUID 提供租户信息)
|
||
role = Role.objects.filter(RoleUUID=role_uuid_bytes).first()
|
||
if role:
|
||
request.current_role = role
|
||
# 从 Role 实例获取租户 UUID(UUIDObj,FormService 会转 bytes)
|
||
request.current_tenant_uuid = role.TenantUUID
|
||
# UserRole.CompanyUUID 是 UUIDField,直接赋值
|
||
if user_role.CompanyUUID:
|
||
request.current_company_uuid = user_role.CompanyUUID
|
||
_logger.info('[ResolveRoleContext] cookie=%s, role=%s, tenant=%s, company=%s',
|
||
role_uuid_str[:36],
|
||
getattr(role, 'RoleName', None) if role else None,
|
||
request.current_tenant_uuid,
|
||
request.current_company_uuid)
|
||
except (ValueError, KeyError, ObjectDoesNotExist) as e:
|
||
_logger.warning('[ResolveRoleContext] cookie=%s 解析失败: %s', role_uuid_str[:36], e)
|
||
except Exception:
|
||
_logger.exception('[ResolveRoleContext] 解析当前角色失败')
|
||
|
||
|
||
class CustomJWTAuthentication(JWTAuthentication):
|
||
"""JWT 认证——使用项目自定义 User 模型(core.User)
|
||
|
||
优先从 Authorization 请求头读取 token;
|
||
若请求头无 token,则回退到 HttpOnly Cookie(AccessToken)。
|
||
|
||
认证成功后立即解析当前角色上下文(ResolveRoleContext),
|
||
确保 request.current_role / current_company_uuid / current_tenant_uuid 可用。
|
||
"""
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self.user_model = User
|
||
|
||
def get_header(self, request):
|
||
"""优先从 Authorization 头获取,回退到 Cookie"""
|
||
header = super().get_header(request)
|
||
if header is not None:
|
||
return header
|
||
# 回退:从 Cookie 读取 access token,构造标准 Bearer 头
|
||
cookie_token = request.COOKIES.get(JWT_ACCESS_COOKIE)
|
||
if cookie_token:
|
||
auth_header_types = self.get_auth_header_types()
|
||
prefix = f'{auth_header_types[0]} '.encode('utf-8') if auth_header_types else b''
|
||
return prefix + cookie_token.encode('utf-8')
|
||
return None
|
||
|
||
def get_auth_header_types(self):
|
||
from rest_framework_simplejwt.settings import api_settings
|
||
return api_settings.AUTH_HEADER_TYPES
|
||
|
||
def authenticate(self, request):
|
||
"""JWT 认证成功后立即解析角色上下文"""
|
||
result = super().authenticate(request)
|
||
if result is None:
|
||
return None
|
||
user, validated_token = result
|
||
# 认证成功后立即解析角色上下文(DRF 会在 authenticate 返回后设置 request.user,
|
||
# 但此时我们已有 user 实例,可直接传入解析)
|
||
ResolveRoleContext(request, user)
|
||
return result
|