feat(msync): UserTokenAuthentication 用户令牌自省 + /me 端点(iframe 身份透传)

This commit is contained in:
TermiNexus committed 2026-08-14 00:33:50 +08:00
1 parent a30e27e4bc
commit 8340be7eb0
3 files changed
+95 -18

No files matched your search

+73 -17
View File
@@ -36,6 +36,50 @@ class ServicePrincipal:
return f'Service:{self.service_name}'
class UserPrincipal:
"""用户主体——员工 iframe 透传的用户上下文(用户令牌自省结果)"""
def __init__(self, payload):
payload = payload or {}
self.UserUUID = payload.get('UserUUID')
self.RoleUUID = payload.get('RoleUUID')
self.CompanyUUID = payload.get('CompanyUUID')
self.TenantUUID = payload.get('TenantUUID')
@property
def is_authenticated(self):
return True
def __str__(self):
return f'User:{self.UserUUID}'
USER_TOKEN_INTROSPECT_PATH = '/api/msync/user/token/introspect/'
def _RemoteIntrospect(path, body):
"""远程自省主服务器令牌端点"""
master_url = str(getattr(settings, 'MSYNC_MASTER_URL', '')).rstrip('/')
if not master_url:
return None
data = json.dumps(body).encode('utf-8')
req = urllib.request.Request(
f'{master_url}{path}',
data=data,
headers={'Content-Type': 'application/json'},
method='POST',
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
result = json.loads(resp.read().decode('utf-8'))
except (urllib.error.URLError, urllib.error.HTTPError, ValueError, OSError) as e:
logger.error(f'令牌自省失败: {e}')
return None
if not result.get('Valid'):
return None
return result.get('Payload') or {}
class SubServerServiceAuthentication(BaseAuthentication):
"""DRF 认证类——校验主服务器 Service 令牌(远程自省)"""
@@ -59,23 +103,35 @@ class SubServerServiceAuthentication(BaseAuthentication):
@staticmethod
def _Introspect(access_token):
"""调用主服务器令牌自省端点"""
master_url = str(getattr(settings, 'MSYNC_MASTER_URL', '')).rstrip('/')
if not master_url:
return _RemoteIntrospect(INTROSPECT_PATH, {'AccessToken': access_token})
class UserTokenAuthentication(BaseAuthentication):
"""DRF 认证类——校验员工用户级令牌(iframe 跨服务器身份透传,远程自省)
Authorization: Bearer <user-token>
入参附带 AudienceService=本服务名,主服务器校验 aud 防令牌串用。
认证通过后 request.user 为 UserPrincipal(含 UserUUID/RoleUUID/
CompanyUUID/TenantUUID),子服务器业务 API 按此鉴权。
"""
def authenticate(self, request):
auth_header = request.headers.get(SERVICE_AUTH_HEADER, '')
if not auth_header.startswith('Bearer '):
return None
body = json.dumps({'AccessToken': access_token}).encode('utf-8')
req = urllib.request.Request(
f'{master_url}{INTROSPECT_PATH}',
data=body,
headers={'Content-Type': 'application/json'},
method='POST',
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read().decode('utf-8'))
except (urllib.error.URLError, urllib.error.HTTPError, ValueError, OSError) as e:
logger.error(f'令牌自省失败: {e}')
token = auth_header[len('Bearer '):].strip()
if not token:
return None
if not data.get('Valid'):
return None
return data.get('Payload') or {}
audience = getattr(settings, 'MSYNC_SERVICE_NAME', '')
payload = _RemoteIntrospect(
USER_TOKEN_INTROSPECT_PATH,
{'AccessToken': token, 'AudienceService': audience},
)
if not payload:
raise AuthenticationFailed('用户令牌验证失败')
return (UserPrincipal(payload), token)
def authenticate_header(self, request):
return 'Bearer'
+1
View File
@@ -6,6 +6,7 @@ app_name = 'sub_server'
urlpatterns = [
path('health/', views.HealthView.as_view(), name='health'),
path('status/', views.StatusPageView, name='status_page'),
path('me/', views.UserMeView.as_view(), name='user_me'),
path('msync/status/', views.MSyncStatusView.as_view(), name='msync_status'),
path('users/', views.UserListView.as_view(), name='user_list'),
path('users/<int:user_id>/', views.UserDetailView.as_view(), name='user_detail'),
+21 -1
View File
@@ -7,7 +7,7 @@ from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated
from .service_auth import SubServerServiceAuthentication
from .service_auth import SubServerServiceAuthentication, UserTokenAuthentication
from .models import BusinessUser, BusinessOrder, SyncLog
from .serializers import (
BusinessUserSerializer,
@@ -341,6 +341,26 @@ def StatusPageView(request):
})
class UserMeView(APIView):
"""当前用户上下文——员工 iframe 访问(用户令牌透传验证)
Authorization: Bearer <user-token>
返回主服务器透传的用户/角色/公司/租户上下文。
"""
authentication_classes = [UserTokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request):
user = request.user
return Response(_ok({
'UserUUID': getattr(user, 'UserUUID', None),
'RoleUUID': getattr(user, 'RoleUUID', None),
'CompanyUUID': getattr(user, 'CompanyUUID', None),
'TenantUUID': getattr(user, 'TenantUUID', None),
}))
class LocalExecuteView(APIView):
"""本地事务执行端点——由主服务器 Saga 编排引擎调用(需主服务器 Service 令牌)"""