Files
SMANAGE/ATestCServer/apps/sub_server/service_auth.py
T

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'