82 lines
2.6 KiB
Python
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 {}
|