feat(msync): UserTokenAuthentication 用户令牌自省 + /me 端点(iframe 身份透传)
This commit is contained in:
1 parent
a30e27e4bc
commit
8340be7eb0
3 files changed
+95
-18
No files matched your search
@@ -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'
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 令牌)"""
|
||||
|
||||
|
||||
Reference in new issue
Block a user