Python / 命令文档
Python Web 开发核心(面试向)
概述
本篇是「Python 面试核心」系列第三篇,覆盖 Web 后端开发主干:HTTP 语义、REST 设计、FastAPI 开发、WebSocket 实时通信、API 鉴权与生产部署。示例用 FastAPI(当前面试与生产的双主流之一)演示,思路对 Flask/Django 同样适用。
所有示例均在 CentOS 测试机(Python 3.10.12 + FastAPI 0.141 + uvicorn)实测通过,涉及的服务均绑定本机随机端口,可反复运行。
Web 面试的分层套路:先考 HTTP 语义(状态码、方法幂等性),再考框架使用(校验、依赖注入),最后考架构(鉴权方案、水平扩展、部署)。
HTTP 基础
REST 的一切建立在 HTTP 语义之上,先补齐协议层:
import functools, http.server, threading
from urllib.request import Request, urlopen
# 起一个本地 HTTP 服务当靶子
Handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory="/tmp")
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
threading.Thread(target=srv.serve_forever, daemon=True).start()
base = f"http://127.0.0.1:{srv.server_port}"
# 请求三要素:方法 + URL + 头;响应三要素:状态码 + 头 + 体
r = urlopen(Request(base + "/", method="GET"), timeout=5)
print(r.status, r.headers.get("content-type", "")[:24]) # 200 text/html ...
r.close()
srv.shutdown()面试要点:
- 状态码分组:2xx 成功、3xx 重定向、4xx 客户端错(401 未认证 / 403 无权限 / 404 不存在 / 422 参数不合法)、5xx 服务端错
- 幂等性:GET/PUT/DELETE 幂等,POST 不幂等 —— 这是「PUT 能重发、POST 要防重」的依据
- GET 参数在 URL,POST 数据在 body;两者都可被篡改,安全不取决于方法而取决于传输层与鉴权
- 无状态:每个请求自带全部凭证(如 Token),服务端不存会话才能水平扩展
REST API 设计
REST = 资源名词 + HTTP 动词 + 状态码表达结果:
# REST 语义速查:URL 是名词复数,动作交给方法 curl -X GET https://api.example.com/v1/items # 列表 200 curl -X GET https://api.example.com/v1/items/1 # 详情 200 curl -X POST https://api.example.com/v1/items # 创建 201 curl -X PUT https://api.example.com/v1/items/1 # 全量更新 200 curl -X DELETE https://api.example.com/v1/items/1 # 删除 204
设计原则清单:
- URL 只放资源与层级,不放动词(
POST /items/1/delete是反面教材,用DELETE /items/1) - 版本进路径或头:
/v1/items,升级不兼容时开 v2 而不是改 v1 - 过滤、分页、排序放查询参数:
GET /items?status=on&page=2&size=20 - 错误响应统一结构:
{"code": "...", "message": "..."},让前端只处理一种错误格式
面试要点:
- 「POST 创建成功返回什么」:201 + Location 头 + 新资源体
- 「PUT 与 PATCH 区别」:PUT 全量替换、PATCH 局部更新
- 「接口怎么保证幂等」:PUT/DELETE 天然幂等;POST 用客户端生成幂等键(Idempotency-Key)去重
FastAPI 入门
类型即校验、注解即文档,是 FastAPI 的核心卖点:
import threading, time, requests, uvicorn
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
items: dict = {} # 内存存储,演示用
class Item(BaseModel): # pydantic 模型:声明即校验
name: str
price: float
@app.get("/items/{item_id}")
def get_item(item_id: int): # 类型不符自动 422,无需手写校验
if item_id not in items:
raise HTTPException(404, "item 不存在")
return items[item_id]
@app.post("/items", status_code=201)
def create_item(item: Item):
new_id = len(items) + 1
items[new_id] = item.model_dump()
return {"id": new_id, **items[new_id]}
# 测试脚本里用线程起 uvicorn,再用 requests 打真实 HTTP
cfg = uvicorn.Config(app, host="127.0.0.1", port=8901, log_level="error")
server = uvicorn.Server(cfg)
threading.Thread(target=server.run, daemon=True).start()
for _ in range(50): # 等服务就绪
try:
requests.get("http://127.0.0.1:8901/docs", timeout=1)
break
except Exception:
time.sleep(0.1)
base = "http://127.0.0.1:8901"
print(requests.post(base + "/items", json={"name": "服务器", "price": 9999}).json())
# {'id': 1, 'name': '服务器', 'price': 9999.0}
print(requests.get(base + "/items/1").json()) # {'name': '服务器', 'price': 9999.0}
print(requests.get(base + "/items/abc").status_code) # 422 路径参数类型校验
print(requests.get(base + "/items/99").status_code) # 404
server.should_exit = True面试要点:
- pydantic 校验失败统一 422 + 详细错误位置,是 FastAPI 与手写框架拉开差距的地方
/docs自动生成交互式 API 文档(OpenAPI/Swagger),文档与代码永不脱节async def与def路由的区别:async 路由跑在事件循环、def 路由自动丢线程池,阻塞库(如同步 DB 驱动)反而该用 def- 依赖注入
Depends:鉴权、DB 会话、分页参数都塞进依赖,测试时可整体替换
WebSocket
HTTP 是请求-响应,WebSocket 是全双工长连接,适合推送、聊天、协作场景:
import asyncio, json, threading, time, uvicorn, websockets
from fastapi import FastAPI, WebSocket
try:
from websockets.asyncio.client import connect # websockets >= 14
except ImportError:
from websockets import connect # websockets < 14
app = FastAPI()
clients: set = set() # 在线连接表
@app.websocket("/ws")
async def ws(sock: WebSocket):
await sock.accept() # 握手确认
clients.add(sock)
try:
while True:
msg = await sock.receive_text()
for c in list(clients): # 广播给所有在线客户端
await c.send_text(json.dumps({"echo": msg}, ensure_ascii=False))
except Exception:
pass # 客户端断开会抛异常,退出循环
finally:
clients.discard(sock)
cfg = uvicorn.Config(app, host="127.0.0.1", port=8902, log_level="error")
server = uvicorn.Server(cfg)
threading.Thread(target=server.run, daemon=True).start()
time.sleep(1.0)
async def client():
async with connect("ws://127.0.0.1:8902/ws") as w:
await w.send("你好 WebSocket")
print("收到:", await asyncio.wait_for(w.recv(), 5))
# 收到: {"echo": "你好 WebSocket"}
asyncio.run(client())
server.should_exit = True面试要点:
- 连接生命周期:握手(HTTP Upgrade)→ 双工收发 → 任一方关闭;服务端要能清理断连(finally + 超时踢人)
- 心跳:客户端定期 ping 或业务层心跳帧,nginx/网关默认会掐空闲连接(proxy_read_timeout)
- 水平扩展难题:连接在不同实例上,广播要经 Redis Pub/Sub 或消息队列中转,不能只发本进程内存里的 clients
- 与 SSE 对比:只需要服务端单向推送时 SSE 更简单(纯 HTTP,自动重连)
API 鉴权
从 API Key 到 JWT/HMAC,核心都是「请求携带可验证的凭证」。下面用标准库手写 HMAC 签名 token,演示 JWT 的核心原理(签名 + 过期 + 防篡改):
import base64, hashlib, hmac, json, threading, time, requests, uvicorn
from fastapi import FastAPI, Header, HTTPException
SECRET = b"kb-demo-secret"
def sign(payload: dict) -> str:
body = json.dumps(payload, separators=(",", ":")).encode()
mac = hmac.new(SECRET, body, hashlib.sha256).digest()
return (base64.urlsafe_b64encode(body).decode()
+ "." + base64.urlsafe_b64encode(mac).decode())
def verify(token: str) -> dict:
try:
b64, mac64 = token.split(".")
body = base64.urlsafe_b64decode(b64)
mac = base64.urlsafe_b64decode(mac64)
want = hmac.new(SECRET, body, hashlib.sha256).digest()
assert hmac.compare_digest(mac, want) # 常量时间比较,防时序攻击
payload = json.loads(body)
assert payload["exp"] > time.time() # 过期即拒
return payload
except Exception:
raise HTTPException(401, "token 无效或过期")
app = FastAPI()
@app.get("/secure")
def secure(x_token: str = Header(default="")): # X-Token 请求头
return {"hello": verify(x_token)["sub"]}
cfg = uvicorn.Config(app, host="127.0.0.1", port=8903, log_level="error")
server = uvicorn.Server(cfg)
threading.Thread(target=server.run, daemon=True).start()
time.sleep(1.0)
base = "http://127.0.0.1:8903"
tok = sign({"sub": "youngsu", "exp": time.time() + 60})
print(requests.get(base + "/secure", headers={"X-Token": tok}).json())
# {'hello': 'youngsu'}
bad = tok[:-4] + "AAAA" # 篡改签名
print(requests.get(base + "/secure", headers={"X-Token": bad}).status_code) # 401
expired = sign({"sub": "youngsu", "exp": time.time() - 1})
print(requests.get(base + "/secure", headers={"X-Token": expired}).status_code) # 401
server.should_exit = True面试要点:
- JWT 三段式:Header.Payload.Signature,前两段只是 Base64 编码(可读、非加密),签名防篡改
- Session vs JWT:Session 状态在服务端(可主动踢人),JWT 状态在令牌里(无状态易扩展,但签发后难撤销)——刷新令牌 + 黑名单是常见折中
- 敏感信息永远不该明文进 payload:Base64 不是加密
- 密码存储用 bcrypt/argon2 慢哈希,不存明文也不做 MD5;比较签名用
hmac.compare_digest防时序攻击 - HTTPS 是底线:Token 一旦明文传输,一切签名机制都是摆设
部署与运行
开发与生产的启动方式不同,重点是 worker 与反向代理:
# 开发:热重载 uvicorn main:app --reload --port 8000 # 生产:多 worker 吃满多核,前置 nginx 做 HTTPS/静态/限流 uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 # 或用 gunicorn 管理 uvicorn worker(更成熟的进程守护) gunicorn main:app -k uvicorn.workers.UvicornWorker -w 4 -b 127.0.0.1:8000
面试要点:
- worker 数经验值:CPU 密集 = 核数;IO 密集(async)可以更高,压测定论
- uvicorn/gunicorn 负责进程与 ASGI,nginx 负责 TLS 终结、静态文件、慢客户端缓冲 —— 各干各的
- WebSocket 经 nginx 反代要加
Upgrade/Connection头透传,并调大空闲超时 - 灰度与回滚:多实例 + 负载均衡滚动重启,健康检查接口(/healthz)是标配
高频面试题速答
| 问题 | 一句话答案 |
|---|---|
| 401 与 403 的区别 | 401 没登录/凭证无效,403 登录了但没权限 |
| PUT 与 POST 区别 | PUT 幂等全量替换指定资源,POST 非幂等创建/提交 |
| JWT 为什么难以撤销 | 签发后自校验无需查库,撤销需引入黑名单或改用短时效 + 刷新令牌 |
| FastAPI 为什么能自动生成文档 | 基于类型注解与 pydantic 模型生成 OpenAPI 规范,Swagger UI 直接可视化 |
| WebSocket 与 HTTP 轮询对比 | 长连接全双工无轮询开销,实时性高,代价是连接管理与扩展复杂度 |
| 接口幂等怎么实现 | PUT/DELETE 天然幂等;POST 用幂等键 + 服务端去重表 |
| 多 worker 下 session 有什么坑 | 内存 session 不共享,多实例要用 Redis 等集中存储 |
| 为什么密码要用 bcrypt 而不是 MD5 | bcrypt 慢且自带盐,能抵御彩虹表与暴力破解,MD5 又快又无盐 |