329 lines
12 KiB
Markdown
329 lines
12 KiB
Markdown
# 请求完整链路
|
||
|
||
本章以一个具体的 API 请求为例,详细描述从浏览器发起 HTTP 请求到返回响应的完整链路,帮助开发者理解每一层的职责和数据流转方式。
|
||
|
||
## 请求链路总览
|
||
|
||
:::tip 链路概览
|
||
一个典型的 API 请求经过以下阶段:浏览器 → Nginx → Tornado → Handler 生命周期(set_default_headers → prepare → handler_method → on_finish)→ 响应返回浏览器
|
||
:::
|
||
|
||
以下以 `POST /api/v1/position/add` 添加岗位为例,逐步拆解完整链路。
|
||
|
||
## Tornado 请求处理模型
|
||
|
||
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() → 统一异常响应 + 日志记录 │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## 第一阶段:网络层
|
||
|
||
### 1. 浏览器发起请求
|
||
|
||
```javascript
|
||
// 前端 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` 请求头
|
||
- JSON 请求体
|
||
|
||
### 2. Nginx 反向代理(生产环境)
|
||
|
||
```nginx
|
||
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` 头。
|
||
|
||
## 第二阶段:Handler 生命周期
|
||
|
||
### 3. 路由匹配
|
||
|
||
Tornado 根据 HTTP 方法 + URL 路径匹配到对应的 Handler 类:
|
||
|
||
```python
|
||
# 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`。
|
||
|
||
### 4. set_default_headers() — 设置响应头
|
||
|
||
```python
|
||
# 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) # 链路追踪 ID
|
||
```
|
||
|
||
- **CORS**:检查 `Origin` 头是否在白名单中,设置 `Access-Control-Allow-*` 响应头
|
||
- **trace_id**:生成唯一追踪 ID,写入响应头和上下文变量
|
||
|
||
### 5. prepare() — JWT 认证
|
||
|
||
```python
|
||
# src/core/base_handler.py
|
||
class BaseHandler(tornado.web.RequestHandler):
|
||
@login_required
|
||
def prepare(self):
|
||
pass
|
||
```
|
||
|
||
`login_required` 装饰器在 `prepare()` 阶段执行认证:
|
||
|
||
```python
|
||
# 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 方法。
|
||
|
||
### 6. Handler 方法 — 权限校验 + 业务处理
|
||
|
||
```python
|
||
# 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)
|
||
```
|
||
|
||
### 7. Service 层 — 业务逻辑
|
||
|
||
```python
|
||
# 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()
|
||
```
|
||
|
||
### 8. Repository 层 — 数据访问
|
||
|
||
```python
|
||
# 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
|
||
```
|
||
|
||
### 9. 响应返回
|
||
|
||
```python
|
||
# 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 响应。
|
||
|
||
### 10. on_finish() — 释放资源
|
||
|
||
```python
|
||
# src/core/base_handler.py
|
||
class BaseHandler(tornado.web.RequestHandler):
|
||
def on_finish(self):
|
||
try:
|
||
db.close() # 兜底关闭数据库会话
|
||
except Exception:
|
||
pass
|
||
```
|
||
|
||
请求结束后关闭数据库会话,避免长连接泄漏。Service 层的 `finally` 块已关闭时此处为幂等 no-op。
|
||
|
||
### 11. 异常处理
|
||
|
||
```python
|
||
# 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()` 中兜底关闭。这种设计使得每一层职责单一、可独立测试。
|