"""MSYNC 服务客户端模块 提供 ServiceClient 类,用于微服务间发起认证请求。 支持自动令牌获取与刷新、OGM 解析、服务发现。 本模块自包含实现(使用 urllib),不依赖 Django。 """ import json import time import logging import urllib.request import urllib.error logger = logging.getLogger(__name__) MSYNC_AUTH_PATH = '/api/msync/auth/token/' MSYNC_AUTH_REFRESH_PATH = '/api/msync/auth/token/refresh/' MSYNC_OGM_RESOLVE_PATH = '/api/msync/ogm/{global_id}/' MSYNC_REGISTRY_PATH = '/api/msync/registry/{name}/' from gvsdsdk.msync.auth import SignPayload, BuildAuthPayload def _GenerateChallenge(): return f'{int(time.time() * 1000)}-{id(object())}' class ServiceClientError(Exception): def __init__(self, Message, StatusCode=None, ResponseBody=None): super().__init__(Message) self.StatusCode = StatusCode self.ResponseBody = ResponseBody class _TokenStore: """内存中的令牌存储""" def __init__(self): self._AccessToken = None self._RefreshToken = None self._ExpiresAt = 0.0 self._Scope = [] @property def AccessToken(self): return self._AccessToken @property def RefreshToken(self): return self._RefreshToken @property def Scope(self): return self._Scope def IsValid(self): return self._AccessToken is not None and time.time() < self._ExpiresAt def Store(self, AccessToken, RefreshToken, ExpiresAt, Scope=None): self._AccessToken = AccessToken self._RefreshToken = RefreshToken self._ExpiresAt = ExpiresAt self._Scope = Scope or [] def Clear(self): self._AccessToken = None self._RefreshToken = None self._ExpiresAt = 0.0 self._Scope = [] class ServiceClient: """微服务通信客户端——用于向其他服务发起认证请求 用法: client = ServiceClient( ServiceName='gerp', ServiceSecret='your-secret', ) client.Authenticate() # 获取其他服务信息 info = client.DiscoverService('crb') # 解析 OGM ogm = client.ResolveOGM('user:abc123') # 向其他服务发请求 response = client.Request('GET', 'https://crb.gvsds.com/api/wallet', Scope=['ledger:read']) """ def __init__(self, ServiceName, ServiceSecret, MasterURL=None): self.ServiceName = ServiceName self.ServiceSecret = ServiceSecret if MasterURL is None: MasterURL = 'https://api.gvsds.com' self.MasterURL = MasterURL.rstrip('/') self._tokens = _TokenStore() @property def IsAuthenticated(self): return self._tokens.IsValid() @property def AccessToken(self): return self._tokens.AccessToken @property def RefreshToken(self): return self._tokens.RefreshToken def Authenticate(self): if not self._Authenticate(): raise ServiceClientError('认证失败') return self def Refresh(self): if not self._RefreshToken(): raise ServiceClientError('刷新失败') return self def _Authenticate(self): challenge = _GenerateChallenge() payload = BuildAuthPayload(self.ServiceName, challenge) signature = SignPayload(payload, self.ServiceSecret) req_body = json.dumps({ 'ServiceName': self.ServiceName, 'Payload': payload, 'Signature': signature, }).encode('utf-8') req = urllib.request.Request( f'{self.MasterURL}{MSYNC_AUTH_PATH}', data=req_body, headers={'Content-Type': 'application/json'}, method='POST', ) try: with urllib.request.urlopen(req, timeout=30) as resp: data = json.loads(resp.read().decode('utf-8')) except urllib.error.HTTPError as e: body = e.read().decode('utf-8', errors='replace') logger.error(f'认证失败 HTTP {e.code}: {body}') return False except Exception as e: logger.error(f'认证异常: {e}') return False if data.get('code') not in (200, 0): logger.error(f'认证业务错误: {data}') return False token_data = data.get('data') or {} self._tokens.Store( AccessToken=token_data.get('AccessToken'), RefreshToken=token_data.get('RefreshToken'), ExpiresAt=token_data.get('ExpiresAt', 0), Scope=token_data.get('Scope', []), ) return self._tokens.AccessToken is not None def _RefreshToken(self): if not self._tokens.RefreshToken: return False req_body = json.dumps({ 'ServiceName': self.ServiceName, 'RefreshToken': self._tokens.RefreshToken, }).encode('utf-8') req = urllib.request.Request( f'{self.MasterURL}{MSYNC_AUTH_REFRESH_PATH}', data=req_body, headers={'Content-Type': 'application/json'}, method='POST', ) try: with urllib.request.urlopen(req, timeout=30) as resp: data = json.loads(resp.read().decode('utf-8')) except Exception as e: logger.error(f'刷新令牌异常: {e}') return False if data.get('code') not in (200, 0): return False token_data = data.get('data') or {} self._tokens.Store( AccessToken=token_data.get('AccessToken'), RefreshToken=token_data.get('RefreshToken'), ExpiresAt=token_data.get('ExpiresAt', 0), Scope=token_data.get('Scope', []), ) return self._tokens.AccessToken is not None def _EnsureAuthenticated(self): if self.IsAuthenticated: return if self._tokens.RefreshToken: self._RefreshToken() return self._Authenticate() def Request(self, Method, URL, Data=None, Headers=None, Scope=None, Timeout=30): self._EnsureAuthenticated() req_headers = { 'Authorization': f'Service {self.AccessToken}', 'Content-Type': 'application/json', 'X-Source-Service': self.ServiceName, } if Scope: req_headers['X-Required-Scope'] = ','.join(Scope) if Headers: req_headers.update(Headers) body = None if Data is not None: if isinstance(Data, dict): body = json.dumps(Data).encode('utf-8') elif isinstance(Data, bytes): body = Data elif isinstance(Data, str): body = Data.encode('utf-8') req = urllib.request.Request(URL, data=body, headers=req_headers, method=Method) try: with urllib.request.urlopen(req, timeout=Timeout) as resp: response_data = resp.read().decode('utf-8') content_type = resp.headers.get('Content-Type', '') if 'application/json' in content_type: return json.loads(response_data) return response_data except urllib.error.HTTPError as e: body_text = e.read().decode('utf-8', errors='replace') if e.code == 401: logger.info('令牌可能已失效,尝试刷新') self._tokens.Clear() self._EnsureAuthenticated() req_headers['Authorization'] = f'Service {self.AccessToken}' retry_req = urllib.request.Request(URL, data=body, headers=req_headers, method=Method) try: with urllib.request.urlopen(retry_req, timeout=Timeout) as resp2: response_data = resp2.read().decode('utf-8') content_type = resp2.headers.get('Content-Type', '') if 'application/json' in content_type: return json.loads(response_data) return response_data except urllib.error.HTTPError as e2: body2 = e2.read().decode('utf-8', errors='replace') raise ServiceClientError( f'重试后仍失败: HTTP {e2.code}', StatusCode=e2.code, ResponseBody=body2, ) raise ServiceClientError( f'请求失败: HTTP {e.code}', StatusCode=e.code, ResponseBody=body_text, ) def ResolveOGM(self, GlobalID): url = f'{self.MasterURL}{MSYNC_OGM_RESOLVE_PATH.format(global_id=GlobalID)}' return self.Request('GET', url, Scope=['ogm:read']) def DiscoverService(self, ServiceName): url = f'{self.MasterURL}{MSYNC_REGISTRY_PATH.format(name=ServiceName)}' return self.Request('GET', url, Scope=['registry:read'])