173 lines
5.5 KiB
Python
173 lines
5.5 KiB
Python
"""模块:服务间认证
|
||
|
||
Django ORM 依赖的认证函数,纯函数已移至 gvsdsdk.msync.auth。
|
||
"""
|
||
|
||
import traceback
|
||
import datetime
|
||
import logging
|
||
|
||
from gvsdsdk.msync.auth import (
|
||
SignPayload,
|
||
VerifySignature,
|
||
GenerateChallenge,
|
||
BuildAuthPayload,
|
||
SignAuthPayload,
|
||
VerifyAuthPayload,
|
||
ServiceTokenEncoder,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _get_master_secret():
|
||
from gvsds.secrets import get as _secret_get
|
||
secret = _secret_get('msync', 'MASTER_SECRET', fallback=None)
|
||
if not secret:
|
||
from django.core.exceptions import ImproperlyConfigured
|
||
raise ImproperlyConfigured('msync MASTER_SECRET 未配置,请在 secrets.ini 或环境变量 MSYNC_MASTER_SECRET 中设置')
|
||
return secret
|
||
|
||
|
||
def IssueServiceToken(service_node, access_lifetime_hours=2, refresh_lifetime_days=30):
|
||
"""为服务节点签发令牌对(AccessToken + RefreshToken)"""
|
||
from gvsds.apps.msync.models import ServiceToken
|
||
|
||
master_secret = _get_master_secret()
|
||
|
||
now_aware = datetime.datetime.now(datetime.timezone.utc)
|
||
access_expires_aware = now_aware + datetime.timedelta(hours=access_lifetime_hours)
|
||
refresh_expires_aware = now_aware + datetime.timedelta(days=refresh_lifetime_days)
|
||
|
||
now_naive = now_aware.replace(tzinfo=None)
|
||
access_expires = access_expires_aware.replace(tzinfo=None)
|
||
refresh_expires = refresh_expires_aware.replace(tzinfo=None)
|
||
|
||
service_scope = service_node.ServiceScope
|
||
if service_scope is None:
|
||
service_scope = ['ogm:read', 'ogm:write', 'registry:read']
|
||
elif isinstance(service_scope, str):
|
||
service_scope = [service_scope]
|
||
|
||
encoder = ServiceTokenEncoder(master_secret)
|
||
access_token = encoder.Encode(
|
||
service_name=service_node.ServiceName,
|
||
scope=service_scope,
|
||
expires_at=access_expires_aware,
|
||
)
|
||
refresh_token = encoder.Encode(
|
||
service_name=service_node.ServiceName,
|
||
scope=service_scope,
|
||
expires_at=refresh_expires_aware,
|
||
)
|
||
|
||
import uuid
|
||
token = ServiceToken(
|
||
TokenUUID=uuid.uuid4().bytes,
|
||
ServiceUUID=service_node,
|
||
AccessToken=access_token,
|
||
RefreshToken=refresh_token,
|
||
TokenScope=service_scope,
|
||
ExpiresAt=access_expires,
|
||
RefreshExpiresAt=refresh_expires,
|
||
)
|
||
token.save()
|
||
return token
|
||
|
||
|
||
def AuthenticateService(service_name, signed_payload, signature):
|
||
"""验证服务身份并签发令牌"""
|
||
from gvsds.apps.msync.models import ServiceNode, ServiceToken
|
||
|
||
try:
|
||
node = ServiceNode.objects.get(ServiceName=service_name, ServiceStatus=1)
|
||
except ServiceNode.DoesNotExist:
|
||
logger.error(f'[AUTH] 服务节点不存在或未激活: {service_name}')
|
||
return None
|
||
except Exception as e:
|
||
logger.error(f'查询服务节点异常 [{service_name}]: {e}\n{traceback.format_exc()}')
|
||
return None
|
||
|
||
import time
|
||
ts = signed_payload.get('ts', 0)
|
||
now = time.time()
|
||
ts_diff = abs(now - ts)
|
||
if ts_diff > 300:
|
||
logger.error(f'[AUTH] 时间戳校验失败: ts={ts}, now={now}, diff={ts_diff:.1f}s')
|
||
return None
|
||
|
||
raw_secret = node.GetSecret()
|
||
if raw_secret is None:
|
||
logger.error(f'服务 {service_name} 密钥加密副本缺失,请重新注册')
|
||
return None
|
||
|
||
# 调试:对比签名
|
||
from gvsdsdk.msync.auth import SignAuthPayload
|
||
expected_sig = SignAuthPayload(signed_payload, raw_secret)
|
||
logger.info(f'[AUTH] 签名对比: expected={expected_sig}, actual={signature}, match={expected_sig == signature}')
|
||
|
||
if not VerifyAuthPayload(signed_payload, signature, raw_secret):
|
||
logger.error(f'[AUTH] 签名验证失败: service={service_name}, payload={signed_payload}')
|
||
return None
|
||
|
||
try:
|
||
ServiceToken.objects.filter(ServiceUUID=node, TokenStatus=1).update(TokenStatus=0)
|
||
except Exception as e:
|
||
logger.error(f'撤销旧令牌异常 [{service_name}]: {e}')
|
||
|
||
try:
|
||
return IssueServiceToken(node)
|
||
except Exception as e:
|
||
logger.error(f'签发服务令牌异常 [{service_name}]: {e}\n{traceback.format_exc()}')
|
||
return None
|
||
|
||
|
||
def ValidateAccessToken(access_token):
|
||
"""验证 AccessToken 并返回载荷信息"""
|
||
from gvsds.apps.msync.models import ServiceToken
|
||
|
||
master_secret = _get_master_secret()
|
||
encoder = ServiceTokenEncoder(master_secret)
|
||
payload = encoder.Decode(access_token)
|
||
if payload is None:
|
||
return None
|
||
|
||
try:
|
||
token_record = ServiceToken.objects.get(AccessToken=access_token, TokenStatus=1)
|
||
if token_record.IsExpired():
|
||
token_record.Revoke()
|
||
return None
|
||
except ServiceToken.DoesNotExist:
|
||
return None
|
||
|
||
return payload
|
||
|
||
|
||
def RefreshServiceToken(refresh_token):
|
||
"""使用 RefreshToken 刷新令牌对"""
|
||
from gvsds.apps.msync.models import ServiceToken, ServiceNode
|
||
|
||
master_secret = _get_master_secret()
|
||
encoder = ServiceTokenEncoder(master_secret)
|
||
payload = encoder.Decode(refresh_token)
|
||
if payload is None:
|
||
return None
|
||
|
||
try:
|
||
old_token = ServiceToken.objects.get(RefreshToken=refresh_token, TokenStatus=1)
|
||
except ServiceToken.DoesNotExist:
|
||
return None
|
||
|
||
if old_token.IsRefreshExpired():
|
||
old_token.Revoke()
|
||
return None
|
||
|
||
service_name = payload.get('sub')
|
||
try:
|
||
node = ServiceNode.objects.get(ServiceName=service_name, ServiceStatus=1)
|
||
except ServiceNode.DoesNotExist:
|
||
return None
|
||
|
||
old_token.Revoke()
|
||
return IssueServiceToken(node)
|