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

82 lines
2.6 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 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):
"""调用主服务器令牌自省端点"""
master_url = str(getattr(settings, 'MSYNC_MASTER_URL', '')).rstrip('/')
if not master_url:
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}')
return None
if not data.get('Valid'):
return None
return data.get('Payload') or {}