Files
paste-framework/paste/web/handler.py
T
2026-06-02 16:26:10 +08:00

250 lines
8.5 KiB
Python
Executable File

import importlib
import json
import logging
from abc import ABC
from collections import namedtuple
from typing import Optional, Union, Any, Type
import tornado.web
from paste.core import config
from paste.db.basemodel import BaseModel
from paste.util.encoder import JsonDumpsEncoder
from paste.core.logging import echo_log
def init_user_class():
"""
从配置文件初始化用户类。默认采用 rbac.RbacUser。
"""
try:
# 若没有配置 RBAC 直接返回 None
_rbac_cfg = config.get_config('rbac.user_class', None)
except AssertionError:
return None
_cfg_user_class: str = config.get_config('rbac.user_class', None)
if _cfg_user_class is not None:
_parts = _cfg_user_class.split('.')
_module_name = '.'.join(_parts[:-1])
_user_module = importlib.import_module(_module_name)
_user_class = getattr(_user_module, _parts[-1])
return _user_class
from paste.rbac.rbac_user import RbacUser
return RbacUser
class RequestHandler(tornado.web.RequestHandler, ABC):
"""
请求控制父类。
"""
route_pattern: Optional[str] = None
"""
URL 路径模式。由装饰器 web.decorators.route 赋值,在 base.Application.load_handler_module 自动加载时调用,作为访问
路径,设置到 Application 中。
"""
user_class: Type[BaseModel] = init_user_class()
"""
用户数据处理类。装饰器 web.decorators.auth_token 执行令牌验证时调用该类,用于创建用户对象,并保存在 current_user 属性中。
注意:这里仅初始化类,而不创建对象。该类允许用户继承扩展,然后自行配置。主要用于执行有关用户的数据操作。
"""
@classmethod
def log(cls, msg: Union[str, Exception], level: int = logging.INFO, is_log_exc: bool = False):
"""
输出日志文本。
:param msg: 消息内容,当是 Exception 对象时,从 args 中取出第一项作为消息
:param level: 消息等级
:param is_log_exc: 是否输出异常信息到日志文件
"""
echo_log(msg=msg, level=level, is_log_exc=is_log_exc)
@classmethod
def dict_to_namedtuple(cls, name, data):
"""
递归转换字典和列表中的字典为 namedtuple 对象。
参数:
name: 用于创建 namedtuple 的名称
data: 要转换的数据,可以是 dict、list 或基本类型
返回:
转换后的 namedtuple 对象或列表
"""
if isinstance(data, dict):
# 处理字典类型
NT = namedtuple(name, data.keys())
return NT(**{
k: cls.dict_to_namedtuple(k, v)
for k, v in data.items()
})
elif isinstance(data, list):
# 处理列表类型:递归转换列表中的每个元素
return [
cls.dict_to_namedtuple(f"{name}_item", item)
if isinstance(item, (dict, list)) else item
for item in data
]
else:
# 基本类型直接返回
return data
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.rule_kwargs = {}
"""
规则参数,用于在控制器和规则之间做数据交换
"""
self.token_payload: dict[str: Any] = {}
"""
令牌配载数据字典。装饰器 web.decorators.auth_token 执行令牌验证时解码并赋值。 在 HandlerRequest 子类中
只要配置 auth_token 装饰即可使用该配载数据。
其结构为::
{
'iss': private_iss,
'iat': datetime.datetime.utcnow(),
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7),
'params': {
'id': user_id,
'username': username
}
}
"""
async def after_auth_token(self, token_payload: dict):
"""
在验证 Token 后调用的函数,子类可覆盖。
:param token_payload: Token 数据项
"""
pass
def token_params(self) -> dict:
"""
取出 Token 中的参数字典。
:return: 参数字典
"""
return self.token_payload.get('params', {})
def token_param(self, key):
"""
取出 Token 参数字典中的参数。
:param key: 参数名称
"""
return self.token_params().get(key, None)
def set_default_headers(self):
"""
设置默认的请求头。
"""
request_headers = dict(self.request.headers)
allow_headers = [
'Accept', 'Content-Type', 'Origin', 'Access-Token', 'ClientId', 'Timestamp', 'Verify-Hash', 'Security-Key'
]
allow_methods = [
'OPTIONS', 'GET', 'POST'
]
allow_origins = [
request_headers.get('Origin', '*')
]
content_type = [
request_headers.get('Content-type', 'application/json')
]
response_header_cfg = {
'Access-Control-Allow-Headers': ','.join(set(allow_headers)),
'Access-Control-Allow-Methods': ','.join(set(allow_methods)),
'Access-Control-Allow-Origin': ','.join(set(allow_origins)),
'Access-Control-Allow-Credentials': 'true',
'Content-type': ','.join(set(content_type)),
}
for _k, _v in response_header_cfg.items():
self.set_header(_k, _v)
def get_current_user(self) -> Any:
if not hasattr(self, '_current_user'):
if self.user_class is not None:
# 设置了用户类,但是未创建对象的,这里默认创建空用户对象
setattr(self, '_current_user', self.user_class())
else:
setattr(self, '_current_user', None)
return self._current_user
def options(self):
"""
处理跨域请求中的 OPTIONS 预检。
"""
self.set_status(status_code=200)
self.finish()
def request_arguments(self):
"""
取得所有请求参数。若 self.request.arguments 中有参数,则优先读取。
若无参数,则从 self.request.body 读取,且该参数必须为 JSON 结构。
:return: 请求参数字典
"""
_args: dict[str: Any] = dict()
if len(self.request.arguments) > 0:
# 按 Form 提交时,从 Form 参数中读取命令,命令参数从 request.arguments 读取
for _n, _v in self.request.arguments.items():
if isinstance(_v, list):
# 对数组进行分解
if len(_v) == 1:
_args[_n] = _v[0].decode("utf-8")
else:
_args[_n] = [__v.decode("utf-8") for __v in _v]
else:
_args[_n] = f"{_v}"
else:
# 非 Form 提交时,从 Body 解析命令,命令参数从 body.params 读取
_body = self.request.body if self.request.body else '{}'
_args = json.loads(_body)
return _args
def response_ok(self, **kwargs):
"""
成功响应内容。
:param kwargs: 参数
"""
self.set_status(status_code=200)
chunk = {'code': 200, 'status': 'OK'}
chunk.update(kwargs)
self.write(json.dumps(chunk, cls=JsonDumpsEncoder, ensure_ascii=False))
self.set_header('Content-Type', 'application/json')
def response_error(self, e: Exception, status_code: int = 200, api_status_code: int = None, **kwargs):
"""
错误响应内容。
:param e: 异常对象
:param status_code: HTTP/HTTPS 响应状态码
:param api_status_code: API 状态码,若不提供则使用 status_code 参数
"""
if api_status_code is None:
api_status_code = status_code
self.set_status(status_code=status_code)
chunk = {'code': api_status_code, 'status': 'error'}
chunk.update(kwargs)
if len(e.args) > 0 and isinstance(e.args[0], str):
chunk['message'] = e.args[0]
if len(e.args) > 1:
if isinstance(e.args[1], dict):
chunk.update(e.args[1])
elif isinstance(e.args[1], list):
chunk['errors'] = e.args[1]
self.write(json.dumps(chunk, cls=JsonDumpsEncoder, ensure_ascii=False))
self.set_header('Content-Type', 'application/json')