138 lines
4.3 KiB
Python
138 lines
4.3 KiB
Python
"""子服务器服务认证
|
|
|
|
主服务器调用子服务器 local 端点时携带 `Authorization: Service <token>`。
|
|
子服务器无法本地验证令牌(无主库 ServiceToken 表 / MASTER_SECRET),
|
|
因此通过主服务器 `/api/msync/auth/introspect/` 远程自省校验。
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import urllib.request
|
|
import urllib.error
|
|
|
|
from django.conf import settings
|
|
from rest_framework.authentication import BaseAuthentication
|
|
from rest_framework.exceptions import AuthenticationFailed
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SERVICE_AUTH_HEADER = 'Authorization'
|
|
SERVICE_AUTH_PREFIX = 'Service'
|
|
INTROSPECT_PATH = '/api/msync/auth/introspect/'
|
|
|
|
|
|
class ServicePrincipal:
|
|
"""服务主体——标识调用方服务"""
|
|
|
|
def __init__(self, service_name):
|
|
self.service_name = service_name
|
|
self.service_scope = []
|
|
|
|
@property
|
|
def is_authenticated(self):
|
|
return True
|
|
|
|
def __str__(self):
|
|
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 令牌(远程自省)"""
|
|
|
|
def authenticate(self, request):
|
|
auth_header = request.headers.get(SERVICE_AUTH_HEADER, '')
|
|
if not auth_header.startswith(f'{SERVICE_AUTH_PREFIX} '):
|
|
return None
|
|
token = auth_header[len(SERVICE_AUTH_PREFIX) + 1:].strip()
|
|
if not token:
|
|
return None
|
|
|
|
payload = self._Introspect(token)
|
|
if not payload:
|
|
raise AuthenticationFailed('服务令牌验证失败')
|
|
|
|
return (ServicePrincipal(payload.get('sub', 'unknown')), token)
|
|
|
|
def authenticate_header(self, request):
|
|
return 'Service'
|
|
|
|
@staticmethod
|
|
def _Introspect(access_token):
|
|
"""调用主服务器令牌自省端点"""
|
|
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
|
|
token = auth_header[len('Bearer '):].strip()
|
|
if not token:
|
|
return None
|
|
|
|
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'
|