加入了 GVSDSDK 模块,进行了 QModel 兼容层的尝试,生产环境可用

This commit is contained in:
2026-06-16 00:13:10 +08:00
parent 9d9cfa53a4
commit 63e0f4edfc
68 changed files with 10256 additions and 1143 deletions

275
gvsdsdk/msync/client.py Normal file
View File

@@ -0,0 +1,275 @@
"""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'])