Become a sponsor

本章以一个具体的 API 请求为例,详细描述从浏览器发起 HTTP 请求到返回响应的完整链路,帮助开发者理解每一层的职责和数据流转方式。
链路概览
一个典型的 API 请求经过以下阶段:浏览器 → Nginx → Tornado → Handler 生命周期(set_default_headers → prepare → handler_method → on_finish)→ 响应返回浏览器
以下以 POST /api/v1/position/add 添加岗位为例,逐步拆解完整链路。
Tornado 通过 Handler 生命周期方法 和 装饰器 实现横切关注点:
┌─────────────────────────────────────────────────────────────────┐
│ Tornado Handler 生命周期 │
│ │
│ ① set_default_headers() → 设置 CORS 头 + trace_id │
│ ② prepare() → @login_required(JWT 认证) │
│ ③ handler method → @permission_required + @check_demo │
│ + @operation_log + 业务逻辑 │
│ ④ on_finish() → db.close()(释放数据库会话) │
│ │
│ 异常时:write_error() → 统一异常响应 + 日志记录 │
└─────────────────────────────────────────────────────────────────┘// 前端 Axios 调用
const res = await request({
url: '/api/v1/position/add',
method: 'post',
data: {
name: '高级工程师',
status: 1,
sort: 10
}
});前端通过 Axios 发起 HTTP POST 请求,携带:
Authorization: Bearer <JWT Token> 请求头Content-Type: application/json 请求头location /api/ {
proxy_pass http://127.0.0.1:8041;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}Nginx 将请求代理到 Tornado 服务,同时附加客户端真实 IP 到 X-Forwarded-For 头。
Tornado 根据 HTTP 方法 + URL 路径匹配到对应的 Handler 类:
# src/api/v1/router.py
def build_routes() -> list:
routes = []
for module in _RESOURCE_MODULES:
routes.extend([(V1_PREFIX + path, handler) for path, handler in module.routes])
routes.append((V1_PREFIX + "/health", HealthHandler))
return routes路由表中 POST /api/v1/position/add 匹配到 PositionAddHandler。
# src/core/base_handler.py
class BaseHandler(tornado.web.RequestHandler):
def set_default_headers(self):
super().set_default_headers()
cros_required(self) # CORS 跨域头
apply_trace_id(self) # 链路追踪 IDOrigin 头是否在白名单中,设置 Access-Control-Allow-* 响应头# src/core/base_handler.py
class BaseHandler(tornado.web.RequestHandler):
@login_required
def prepare(self):
passlogin_required 装饰器在 prepare() 阶段执行认证:
# src/common/middleware/authentication.py
def login_required(func):
def wrapper(self, *args, **kwargs):
request_url = self.request.path
# 白名单(/login、/captcha)和 OPTIONS 预检直接放行
if request_url not in IGNORE_URL and self.request.method != "OPTIONS":
# 1. 从 Authorization 头提取 Bearer Token
success, access_token, _ = get_access_token(self.request)
if not success or not access_token:
return R.failed(self, code=401, msg="登录过期")
# 2. JWT 解密验证(签名 + 有效期)
result = parse_payload(access_token)
if result["code"] != 0:
return R.failed(self, code=401, msg="登录过期")
# 3. 黑名单校验(Redis,不可用时 fail-open 放行)
if is_token_blacklisted(access_token):
return R.failed(self, code=401, msg="登录过期")
# 4. 将用户信息写入上下文变量
data = result["data"]
current_user_id.set(int(data["userId"]))
current_username.set(data.get("username", ""))
current_realname.set(data.get("realname", ""))
return func(self, *args, **kwargs)
return wrapper认证失败时直接返回 {"code": 401, "msg": "登录过期"},不进入后续 Handler 方法。
# src/modules/position/handlers.py
class PositionAddHandler(BaseHandler):
@permission_required("sys:position:add") # ① RBAC 权限校验
@check_demo # ② 演示模式拦截
@operation_log("岗位管理", "添加") # ③ 操作日志记录
async def post(self):
return await position_service.add(self) # ④ 调用 Service 层装饰器执行顺序(从外到内):
① permission_required → 校验用户是否拥有 sys:position:add 权限
├── user_id == 1(admin)→ 直接放行
└── 其他用户 → 查询权限列表(Redis 缓存)→ 匹配则放行,否则返回 403
② check_demo → TORNADO_DEMO=True 时返回 "演示环境,暂无操作权限"
③ operation_log → 记录操作日志(请求参数、操作人、IP、耗时等)
④ handler method → 调用 position_service.add(self)# src/core/base_service.py
class BaseService:
async def add(self, handler) -> R:
try:
# 1. 解析请求体 JSON + Pydantic 校验
form = self._validate(handler, self.create_schema)
data = form.model_dump()
# 2. 唯一性校验
err = self._check_unique(form)
if err:
return R.failed(handler, err)
# 3. 前置扩展钩子
self._before_add(handler, data)
# 4. 组装入库字段
fields = self._build_create_fields(data)
# 5. 调用 Repository 创建记录
self.repo.create(self.model(**fields))
# 6. 返回成功响应
return R.ok(handler, msg="添加成功")
finally:
self.repo.close()# src/core/base_repository.py
class BaseRepository:
def create(self, obj: T) -> T:
# 自动写入创建人
if hasattr(obj, "create_user"):
obj.create_user = str(self._uid())
self.db.add(obj)
self.db.commit()
return obj# src/core/response.py
def ok(self, data=None, msg="操作成功", code=0, **kwargs):
body = {"code": code, "data": data, "msg": msg, "ok": True}
if kwargs:
body.update(kwargs)
self._api_result = body
self.finish(body) # Tornado 写出 JSON 响应R.ok() / R.failed() 内部调用 self.finish(body),Tornado 自动序列化为 JSON 并发送 HTTP 响应。
# src/core/base_handler.py
class BaseHandler(tornado.web.RequestHandler):
def on_finish(self):
try:
db.close() # 兜底关闭数据库会话
except Exception:
pass请求结束后关闭数据库会话,避免长连接泄漏。Service 层的 finally 块已关闭时此处为幂等 no-op。
# src/core/base_handler.py
class BaseHandler(tornado.web.RequestHandler):
def write_error(self, status_code, **kwargs):
exc = kwargs.get("exc_info", (None, None, None))[1]
if isinstance(exc, BaseAppError):
# 业务异常:按类型分级记录日志
self.set_status(exc.status_code)
self.finish({"code": exc.code, "data": None, "msg": exc.msg, "ok": False})
else:
# 未预期异常:记录完整堆栈
self.set_status(500)
self.finish({"code": 500, "data": None, "msg": "服务器内部错误", "ok": False})| 异常类型 | 触发场景 | 响应 |
|---|---|---|
AuthError | JWT 过期 / 无权限 | {"code": 401, "msg": "登录过期"} |
PermissionDeniedError | 权限不足 | {"code": 403, "msg": "权限不足"} |
ValidationError | Pydantic 校验失败 | {"code": 1, "msg": "field: message"} |
BusinessError | 业务逻辑错误 | {"code": 1, "msg": "..."} |
NotFoundError | 记录不存在 | {"code": 404, "msg": "记录不存在"} |
Exception | 未捕获异常 | {"code": 500, "msg": "服务器内部错误"} |
[浏览器]
│ POST /api/v1/position/add
│ Authorization: Bearer <token>
▼
[Nginx] ── 反向代理转发
│
▼
[Tornado 路由匹配] ── 匹配 PositionAddHandler
│
▼
[set_default_headers()] ── 设置 CORS 头 + trace_id
│
▼
[prepare() + @login_required] ── JWT 认证
│ ├─ 白名单(/login、/captcha)→ 跳过
│ ├─ Token 无效/过期 → 返回 {code:401, msg:"登录过期"}
│ ├─ Token 在黑名单中 → 返回 {code:401, msg:"登录过期"}
│ └─ 认证通过 → current_user_id / current_username / current_realname 写入上下文
│
▼
[@permission_required("sys:position:add")] ── RBAC 权限校验
│ ├─ user_id == 1 → 直接放行
│ ├─ 权限不足 → 返回 {code:403, msg:"权限不足"}
│ └─ 权限通过 → 继续
│
▼
[@check_demo] ── 演示模式拦截
│ ├─ TORNADO_DEMO=True → 返回 {code:1, msg:"演示环境,暂无操作权限"}
│ └─ 非演示模式 → 继续
│
▼
[@operation_log("岗位管理", "添加")] ── 操作日志装饰器
│
▼
[async def post(self)] ── Handler 方法
│
▼
[position_service.add(self)] ── Service 层
├─ parse_json(handler) ── 解析请求体 JSON
├─ validate(schema, data) ── Pydantic 校验
├─ _check_unique(form) ── 唯一性校验(DB 查询)
├─ _before_add(handler, data) ── 前置扩展钩子
├─ _build_create_fields(data) ── 组装入库字段
└─ repo.create(model(**fields)) ── Repository 层
│
▼
[BaseRepository.create(obj)]
├─ obj.create_user = str(uid) ── 自动写入创建人
├─ db.add(obj) + db.commit() ── 提交事务
└─ return obj
│
▼
[R.ok(handler, msg="添加成功")] ── 统一响应
├─ self._api_result = body ── 保存结果供日志解析
└─ self.finish(body) ── 写出 JSON 响应
│
▼
[on_finish()] ── 释放资源
└─ db.close() ── 关闭数据库会话
│
▼
[浏览器] ← { "code": 0, "data": null, "msg": "添加成功", "ok": true }Tornado 的请求处理通过 Handler 生命周期方法(set_default_headers → prepare → handler_method → on_finish)和装饰器(@permission_required、@check_demo、@operation_log)实现横切关注点。认证在 prepare() 阶段完成,权限/演示模式/操作日志通过装饰器在 Handler 方法执行前校验,数据库会话在 on_finish() 中兜底关闭。这种设计使得每一层职责单一、可独立测试。