Python / 命令文档

Python Web 开发核心(面试向)

$ python 8 个章节 Linux 命令详解

概述

本篇是「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 而不是 MD5bcrypt 慢且自带盐,能抵御彩虹表与暴力破解,MD5 又快又无盐

同分类 · 其他命令文档