FastAPI 与后端篇
对应课件:Python框架指南/01-14。AI 应用开发岗的 JD 里几乎都写着"会 FastAPI/会后端"——面试官要确认你能独立交付服务,而不只是跑脚本。本篇 6 题对准 AI 岗最常考的后端点:异步路由(阻塞事…
对应课件:Python框架指南/01-14。AI 应用开发岗的 JD 里几乎都写着"会 FastAPI/会后端"——面试官要确认你能独立交付服务,而不只是跑脚本。本篇 6 题对准 AI 岗最常考的后端点:异步路由(阻塞事件循环是重灾区)、Pydantic 校验、依赖注入、SSE 流式(LLM 应用的标配接口)、中间件与异常、认证。
Q01 · FastAPI 的路由什么时候必须写 async def?写错了会怎样?
难度:★★★★ | 频率:极高 | 对应课件:Python框架指南/01-FastAPI入门与ASGI
面试官:FastAPI 里有的路由写 async def,有的写普通 def——区别是什么?我写了个 async 路由里面调 requests 库,出了什么问题?
第 1 层追问:两条执行路径
先说两种路由分别跑在哪。
参考回答:FastAPI(ASGI 框架)对两种路由有完全不同的执行安排。async def 路由:直接跑在事件循环里——你拥有极高的并发容量(单进程万级连接),代价是绝对不能阻塞(见第 2 层)。普通 def 路由:框架自动把它丢进线程池执行(anyio 缺省几十个线程)——阻塞了也只占一个线程,事件循环不受影响,但并发上限就是线程数。所以选型逻辑:路由体是同步代码/同步库 → 写 def(让框架兜底);路由体全程异步(异步客户端、异步 DB)→ 写 async def(拿满并发红利)。
第 2 层追问:经典事故
async 路由里用 requests 会怎样?
参考回答:整服务瘫痪级事故。requests 是同步阻塞库——async 路由跑在事件循环上,一个 requests.get(等几十秒 LLM 响应)期间事件循环被卡死,整个服务的所有请求(包括健康检查)全部冻结。症状极迷惑:并发量一上来全接口变慢,CPU 却很闲。纪律:async 路由里的一切调用都必须是异步的——LLM 用 AsyncOpenAI/httpx.AsyncClient、HTTP 用 httpx 异步模式、sleep 用 asyncio.sleep、DB 用异步驱动(SQLAlchemy async session)。这个坑和 Python 篇 Q08(阻塞冻结 loop)是同一个原理在 Web 层的重演,面试官常故意串着问。
第 3 层追问:诊断与修复
线上怀疑有路由在阻塞事件循环,怎么定位?
参考回答:三个手段。一,看症状指纹:所有接口延迟一起涨(不是某个接口慢)、健康检查也超时、CPU 低——高度怀疑事件循环被卡。二,代码扫描:全局搜 async def 路由体内的同步调用(requests、time.sleep、open 同步、重 CPU 循环)。三,埋点证实:中间件里对比"事件循环延迟"(asyncio 的 loop.time 定期心跳探针)和请求处理时长——循环心跳变慢 = 有东西在阻塞。修复三板斧:换异步库;改路由为 def 让线程池兜底(同步库场景的快速止血);CPU 密集段丢 run_in_executor/asyncio.to_thread。
第 4 层追问:ASGI 视角
为什么 FastAPI 能同时吃 HTTP 和 WebSocket?ASGI 和 WSGI 差在哪?
参考回答:因为 ASGI 是异步协议。WSGI(Flask 时代)是同步模型:一个请求占一个 worker,长连接(WebSocket、SSE)会占死 worker,天生做不了流式推送。ASGI 的事件模型:等待不占执行权——一个事件循环上挂万级连接,WebSocket 消息、SSE 逐块推送都是"事件来了处理一下"。这正是 LLM 应用的刚需:流式输出意味着一条 HTTP 连接挂着几十秒慢慢吐 token——同步模型下一个用户流式对话就吃掉一个 worker,十个用户并发就瘫痪;ASGI 下这只是事件循环里的一万个挂起协程之一。Uvicorn 是跑 ASGI 应用的服务器,Starlette 是 FastAPI 底下的 ASGI 工具层——三层关系说清楚,框架选型的"为什么"就闭环了。
考察意图与踩坑点
- 考察意图:这是 AI 应用岗后端面试的头号题——"会不会把 LLM 应用部署成真服务"一锤定音;阻塞事件循环是最常见的事故源,亲历过的人能讲到症状指纹级。
- 踩坑点:"async def 就是快"(不知道代价和纪律);说不出 requests 事故的机制;分不清 ASGI/WSGI 与流式的关系。
- 一句话总结:async 路由跑事件循环不能阻塞、def 路由自动进线程池;async 里用同步库 = 全服务冻结;ASGI 的"等待不占执行权"是流式和长连接的根基。
Q02 · Pydantic 在 FastAPI 里起什么作用?和 LLM 应用有什么关系?
难度:★★★ | 频率:高 | 对应课件:Python框架指南/03-Pydantic数据校验与配置
面试官:FastAPI 为什么拿 Pydantic 当核心?请求体校验的流程说下。你做 LLM 应用时 Pydantic 还能干什么?
第 1 层追问:校验的角色
先说它在 FastAPI 里的位置。
参考回答:Pydantic 是 FastAPI 的数据校验与类型系统:请求体声明为 Pydantic 模型后,框架在进路由函数前自动完成——解析 JSON → 类型强制转换 → 字段校验 → 生成带类型提示的模型实例。{"age": "18"} 会自动转成 int 18;类型不符/缺字段/超约束直接返回 422 + 逐字段错误详情(FastAPI 的开箱体验一半来自这里)。三层收益:边界安全(脏数据进不了业务层——课程"统一异常处理"的地基);类型即文档(OpenAPI/Swagger 自动生成);开发体验(编辑器里 order.items 全程有类型提示)。
第 2 层追问:校验进阶
自定义校验规则怎么写?配置怎么管理?
参考回答:字段级 field_validator(v2 语法):对单字段加规则——@field_validator("price") def check(cls, v): if v < 0: raise ValueError(...);模型级 model_validator:跨字段约束(结束日期必须晚于开始日期)。配置管理用 BaseSettings:字段从环境变量自动读取(OPENAI_API_KEY → settings.openai_api_key),.env 文件支持——把密钥从代码里赶出去的标准姿势,课程 demo 的 .env.example 就是这个模式:仓库里放 example 模板、真实值进 .env 且 gitignore。生产铁律:配置和密钥永远不硬编码。
第 3 层追问:LLM 应用里的第二个身份
在 AI 应用里 Pydantic 还有什么用?
参考回答:它是模型输出的第一道闸——这个身份比 Web 校验更关键。LLM 输出是概率性文本:with_structured_output(模型类) 让模型按 schema 生成、结果直接转成 Pydantic 实例;类型不对、枚举越界、缺字段全部在校验层拦截,而不是带着脏数据跑进业务逻辑。再叠一层语义校验(LangChain 篇 Q03 的三层工程):格式合法后再查业务约束(日期在未来、城市在枚举内)。同一个工具在系统的两个边界各守一关:入口拦用户的脏输入,出口拦模型的脏输出——"所有跨边界的数据都要过校验"这个原则,在 AI 应用里因为模型不可靠而加倍成立。
第 4 层追问:性能与版本
Pydantic v2 的变化知道吗?
参考回答:知道核心点即可:v2 把校验核心用 Rust 重写(pydantic-core),比 v1 快数倍到数十倍——校验高频路径(每个请求、每条 LLM 输出)收益真实。API 层面 v2 有破坏性变化:validator 从 @validator 改 @field_validator、.dict() 改 .model_dump()、BaseSettings 拆去了 pydantic-settings 包。迁移意识:接手项目先看 pydantic 版本再抄写法,v1/v2 代码混抄是最常见的"抄了就报错"来源。深层认知:校验开销在 LLM 应用里几乎可以忽略——一次校验微秒级,一次模型调用秒级,性能敏感点永远在模型侧不在校验侧(别在错误的层优化)。
考察意图与踩坑点
- 考察意图:Pydantic 是 FastAPI 技术栈的黏合剂,也是 AI 应用校验体系的承重墙;"两个身份"(入口拦用户、出口拦模型)的答案结构直接对应课程的工程思想。
- 踩坑点:只知道"自动校验"说不出 422 流程;不知道 BaseSettings 管密钥;想不到 Pydantic 和 LLM 结构化输出的关系(AI 岗的隐性考点)。
- 一句话总结:Pydantic 在入口拦用户的脏输入(422+OpenAPI)、在出口拦模型的脏输出(with_structured_output);密钥进 BaseSettings/.env;v2 Rust 内核、性能无关痛痒因为瓶颈在模型。
Q03 · 依赖注入是什么?FastAPI 的 Depends 解决什么问题?
难度:★★★ | 频率:高 | 对应课件:Python框架指南/08-依赖注入
面试官:FastAPI 到处是 Depends。依赖注入解决什么问题?yield 依赖又是干什么的?
第 1 层追问:解决的问题
先说不用 DI 会怎样。
参考回答:不用依赖注入,路由函数里得自己 new 出所有协作对象:db = Session(...)、settings = load_config()、llm = OpenAI(key...)——四个代价:重复(每个路由一段样板);耦合(路由写死具体实现,换实现要改 N 处);难测(单测没法替身,一测就连真库);资源失控(连接谁关?异常路径也关吗?)。依赖注入把"要什么"(参数声明)和"怎么造"(provider 函数)分离:路由签名里声明依赖,框架注入现成实例——依赖的构造、复用、销毁、替身全部集中管理。这是控制反转思想:对象不来"拿"依赖,而是"被喂"依赖。
第 2 层追问:FastAPI 的形态
具体怎么用?
参考回答:def get_db(): ... + 路由参数 db = Depends(get_db)。三个进阶形态:一,依赖树——依赖可以依赖别的依赖(get_service 依赖 get_db 依赖 get_settings),框架按需构建整棵树,配置→引擎→会话→业务的装配链一条声明搞定。二,同请求缓存——同一个依赖在一次请求里被引用多次只构建一次(多处 Depends(get_db) 拿到同一会话),既省资源又保证"同请求同实例"的语义。三,鉴权即依赖——current_user = Depends(verify_token):校验失败在依赖里直接抛 401,路由体内干净得看不到鉴权代码;这是 DI 在 FastAPI 里最漂亮的用法(JWT 题会展开)。
第 3 层追问:yield 依赖
def 依赖里带 yield 是什么?
参考回答:资源生命周期管理——yield 前是"获取"(相当于 enter),yield 后是"清理"(相当于 exit):
def get_db():
db = SessionLocal()
try:
yield db # 请求处理期间,db 交给路由
finally:
db.close() # 请求结束(无论成功异常)必执行
异常路径也保证清理——这正是 with 语句的 try/finally 语义被搬进了依赖系统(Python 篇 Q07 的呼应)。课程里数据库会话就是这么写的:"一个请求一个会话" 的资源纪律,用声明式写法落地。yield 依赖 + 同请求缓存组合出标准模式:db: Session = Depends(get_db) 出现在所有路由签名里,没人手写 close。
第 4 层追问:测试视角
依赖注入对测试意味着什么?
参考回答:可替换性是 DI 的终极回报。测试时 app.dependency_overrides[get_db] = override_get_db——真库换成内存 SQLite/假会话,路由代码零改动;LLM 依赖同样可整体替换成桩模型(返回预设响应的假客户端)——不用 mock 补丁去改内部,直接换供给,测试稳定且不脆。这也是依赖注入的检验标准:"想替身时要改多少业务代码"——答案应该是零。工程判断:DI 不是炫技,是为"会变的边界"(数据库、外部 API、模型服务)预留的替换缝;纯内部函数不用硬上。
考察意图与踩坑点
- 考察意图:DI 是"写脚本"和"做工程"的分水岭;yield 依赖和测试替身两问,能筛出真正交付过服务的人。
- 踩坑点:把 Depends 说成"传参数的语法糖";不知道同请求缓存和依赖树;说不清 yield 依赖和 finally 的关系。
- 一句话总结:DI 分离"要什么"和"怎么造"——依赖树自动装配、同请求缓存、鉴权即依赖、yield 管资源生命周期、测试靠 override 整体换桩。
Q04 · LLM 流式接口为什么用 SSE 而不是 WebSocket?
难度:★★★ | 频率:极高(AI 岗专属高频)| 对应课件:Python框架指南/14-流式响应SSE与WebSocket
面试官:你的 LLM 应用前端逐字显示模型输出,后端接口怎么设计的?为什么选 SSE 不选 WebSocket?
第 1 层追问:SSE 是什么
先说方案本身。
参考回答:SSE(Server-Sent Events):基于 HTTP 的单向服务端推送协议——响应头 Content-Type: text/event-stream,连接保持,服务端按 data: {...}\n\n 的帧格式持续写事件,客户端(浏览器原生 EventSource 或 fetch 流式读取)逐块接收。FastAPI 里就是 StreamingResponse 包一个生成器/异步生成器——模型吐一个 chunk,yield 一个 SSE 帧,天然咬合(生成器 → SSE 的管线在 Python 篇 Q03 和 LangChain 篇 Q02 都出现过,这条链是流式的完整闭环)。
第 2 层追问:为什么不是 WebSocket
双向的 WebSocket 不是更强吗?
参考回答:用不到的双向是纯开销。对比四个维度:协议成本——WS 要升级握手、独立帧协议、自定义重连逻辑;SSE 就是 HTTP,现有基础设施全兼容(网关、代理、鉴权头、CDN)。方向匹配——LLM 对话的流量本质是"请求一次、单向流回":输入一个 prompt,输出一条 token 流,SSE 的单向模型精确匹配。断线恢复——SSE 协议内置自动重连 + Last-Event-ID 续传,浏览器白送;WS 的重连和状态恢复全部自己写。WebSocket 的正当场景:真双向高频——协作编辑、游戏、多人聊天室、语音流。判别一句话:服务端推、客户端只发一两次请求 → SSE;客户端也要高频推 → WS。
第 3 层追问:断流与保活
LLM 生成到一半连接断了,怎么办?中间的 nginx 把连接掐了怎么办?
参考回答:两个真实生产问题。断流:LLM 流不能真正续传(模型是自回归状态,Last-Event-ID 只能定位"客户端收到哪",服务端没法从那个 token 继续生成——除非服务端缓存完整响应)。工程方案三选一:重新生成(简单,用户体验是重看一遍)、后台任务续跑(断流后任务转后台生成完存库,用户重连拉全量——把流式降级成异步任务,系统设计篇 Q05 的提交-查询模式)、服务端缓冲+按 id 补发(连接断了任务不停,重连时先补已生成部分再继续流)。保活:代理/网关默认掐空闲连接(nginx 缺省 60s),生成间隙或长思考期连接"看似空闲"——定期发 SSE 注释帧(: ping\n\n)做心跳,是标准防掐手段。结束标记:流结束要发显式的 [DONE] 或结束事件并关闭——客户端靠它区分"正常结束"和"断流"。
第 4 层追问:心跳之外的坑
上线前还要注意什么?
参考回答:三个高频坑。一,缓冲层吞流式:某些中间件/WSGI 化的部署会把响应缓冲到完整才发——流式变"一次性到货";自测方法:curl 直接看是不是逐块到。二,异步一致性:StreamingResponse 的生成器必须全异步——里面出现同步阻塞(Python 篇 Q08 的坑),流式变成"卡住-爆发"的节奏。三,客户端消费姿势:fetch 读 SSE 不能用普通 await res.text()(会等全量)——要用 res.body 的流式 reader 逐块解帧;浏览器 EventSource 自动处理但只支持 GET(POST 请求要 fetch 流)。这层答出来 = 真上过线的流式接口。
考察意图与踩坑点
- 考察意图:SSE 是 AI 应用的标配接口,这题出现频率随 LLM 应用普及暴涨;"为什么不是 WS"考技术选型,断流/保活考生产经验。
- 踩坑点:答"WebSocket 更高级所以用 WS"(方向不匹配);不知道断流不能续传的真实原因(自回归状态);没听说过心跳防掐和缓冲吞流。
- 一句话总结:SSE = HTTP 单向流,方向精确匹配 LLM 输出、自动重连白送;模型自回归决定断流只能重生成或转后台;心跳防代理、异步防卡顿。
Q05 · 中间件的执行顺序?统一异常处理怎么设计?
难度:★★★ | 频率:中 | 对应课件:Python框架指南/09-统一异常处理、10-中间件
面试官:FastAPI 的中间件写洋葱模型,注册顺序和执行顺序什么关系?全服务的错误响应格式怎么统一?
第 1 层追问:洋葱模型
中间件怎么运转?
参考回答:洋葱模型:请求进来逐层穿透中间件到路由,响应再原路穿回来——每层中间件包住"里面的所有东西",处理请求在剥进去的路上、处理响应在剥出来的路上。注册顺序和执行顺序是反的:先注册的中间件在最外层(先看到请求、最后看到响应)。用 @app.middleware("http") 或 app.add_middleware(Custom) 注册,内部形态就是"调 call_next 拿响应、前后做自己的事"。实测心法:请求日志在最外层(能看到全链路)、计时器包住业务(测的才是处理时长)——顺序写反,日志就记不到真实耗时,这是中间件顺序最有实战意义的坑。
第 2 层追问:统一异常处理
各路由抛不同的异常,怎么让客户端拿到一致的错误格式?
参考回答:@app.exception_handler(异常类)——按异常类型注册全局处理器:路由/依赖里抛的异常被框架拦截,转成统一结构的 JSONResponse(code、message、request_id、可读的 detail)。设计标准三件:一,业务异常体系——自定义异常类层级(AppError 基类 → NotFoundError、QuotaExceededError...),路由里 raise 语义化异常而不是返回裸 dict;二,兜底处理器——挂一个 Exception 兜底:未预期异常统一 500 + 服务器日志记全栈(对外不泄栈,对内不丢栈);三,错误码设计——错误码是给调用方程序消费的契约,要稳定、分层(4xx 参数/权限/配额,5xx 系统),不能靠 message 文案传达语义。这层是"接口质量"的直接展示面,也是课程第九章的主题。
第 3 层追问:横切关注点组合
日志、trace id、耗时、鉴权怎么摆进中间件体系?
参考回答:按关注点选层,不是全塞中间件。最外层:trace-id 中间件——生成 request_id 注入 contextvar,日志全链路带同一个 id(排障的钥匙:一个请求的所有日志一 grep 全出);请求日志 + 耗时统计也在外层。中间层:CORS、限流(可插拔)。靠内:鉴权用依赖注入而不是中间件(Q03 的结论——依赖可以精确到路由级"哪些接口要鉴权",中间件是全局一刀切,粒度不够)。责任清单:trace-id/日志/耗时=中间件;鉴权/参数校验=依赖;错误格式=exception_handler;三层各司其职,路由函数里只剩业务。这套组合是"接口工程完整度"的样板,答出来就是做过正经服务的人。
第 4 层追问:性能与陷阱
中间件有什么性能或行为上的坑?
参考回答:两个值得知道的。一,BaseHTTPMiddleware 的开销:这类基类实现(@app.middleware 装饰器底层)会为每个请求包一层 anyio 任务和流的包装,高 QPS 下有可测量的开销——极致性能场景用纯 ASGI 中间件(raw = app; async def __call__(scope, receive, send) 直接管消息);一般业务量级不用担心,先测量再优化。二,response 不可变的坑:中间件里 call_next 返回的 StreamingResponse 不能直接改 body(流已被消费/未生成完)——想改响应头可以(response.headers[...]),想改 body 必须在路由层做或重新组装流。"中间件里改不了流式响应的 body"这个坑踩过一次就终身免疫。
考察意图与踩坑点
- 考察意图:中间件+异常处理考"服务工程完整度"——AI 应用岗常要一个人扛起整套接口,这题是交付能力的证明。
- 踩坑点:注册/执行顺序说反;鉴权塞中间件不放依赖;不知道 exception_handler 兜底;改流式 body 的坑没概念。
- 一句话总结:洋葱反序(先注册在最外)、异常靠 exception_handler 统一格式、trace-id 进 contextvar、鉴权归依赖;流式响应的 body 在中间件里改不了。
Q06 · JWT 认证的原理?过期了怎么办?
难度:★★★ | 频率:中 | 对应课件:Python框架指南/11-认证与JWT
面试官:你们的 API 用 JWT 认证。它的原理是什么?"无状态"是什么意思?token 过期了用户还在填表单,怎么处理?
第 1 层追问:原理
JWT 的结构和工作机制。
参考回答:JWT 是自验证的令牌:三段式 header.payload.signature——header 声明算法,payload 装声明(user_id、角色、过期时间),signature = 服务端用密钥对前两段签名。验证时服务端用同一密钥重算签名比对——篡改 payload(改 user_id 提权)会让签名对不上,立即失效。"无状态"的含义:验证只需密钥,不需要查库/查会话表——服务可以水平扩容、无共享存储,任何实例都能独立验签。不可变双刃剑:payload 签发后不能改(改了签名失效)——JWT 里不能放可变数据,且签发后无法主动作废(没有"注销"按钮,只能等过期)。
第 2 层追问:FastAPI 里的接法
代码层面怎么组织?
参考回答:鉴权做成依赖(Q03 的最佳拍档):OAuth2PasswordBearer(tokenUrl="/login") 声明取 token 的位置,依赖函数里 decode 验签 → 查出 user → 失败抛 401。路由签名一行 user = Depends(get_current_user) 完成声明式鉴权——哪些接口要、哪些不要,粒度精确到路由;依赖链还能再叠(admin = Depends(require_admin) 里复用 get_current_user)——权限模型变成可组合的依赖树。安全纪律两条:密钥进环境变量(Pydantic Settings);payload 只放 user_id/角色这类身份标识,不放敏感信息(JWT 的 payload 只是 Base64,能被任何人解开看——签名防篡改不防偷看)。
第 3 层追问:过期与刷新
access token 过期了怎么办?一直不发新 token 会怎样?
参考回答:双 token 机制。access token:短命(15 分钟-1 小时),每个请求带——泄露了危害窗口小。refresh token:长命(数天-数周),只在刷新接口用一次换新 access token——平时不暴露在业务请求里。流程:access 过期 → 前端静默拿 refresh 换新 → 重放原请求——用户填了一半的表单无感续期(用户视角"没掉线")。refresh 也过期才真正重新登录。为什么这么设计:access 的泄露面(每个请求头)决定它必须短命;refresh 的暴露面小(专用接口)所以可以长——暴露面和寿命成反比是 token 设计的核心权衡。配套服务端细节:refresh 可以建"撤销清单"(存 Redis)——补上 JWT"无法主动作废"的短板;轮换(每次刷新发新 refresh 旧的作废)防重放。
第 4 层追问:LLM 应用的特有考题
AI 应用的认证有什么特殊的地方?
参考回答:三个 AI 场景特有的点。一,凭证分层:用户 JWT 鉴"人",调 LLM 服务的 API key 是另一层凭证(服务身份)——两层不能混:用户 token 永远不该到模型服务,模型 key 永远不下发前端;凭证最小化 + 各守各层。二,配额绑定认证:AI 调用是真金白银,认证完紧接配额检查(依赖链:Depends(get_current_user) → Depends(check_quota))——超限的 429/402 响应在进模型前拦下,不是烧完 token 才发现欠费。三,Agent 工具的凭证:Agent 替用户执行工具时,工具的权限应继承用户权限(用用户的凭证跑用户能干的事)而不是给 Agent 一个超级凭证——这是 agent核心/15"权限控制"的落地:Agent 永远不该比用户更有权。
考察意图与踩坑点
- 考察意图:认证是后端面试的常规必考;AI 岗加考"凭证分层和配额前置"——答出这两点就区分于普通后端候选人。
- 踩坑点:以为 payload 加密(只是编码);不知道 JWT 无法主动作废;access/refresh 的权衡说不出"暴露面"逻辑;用户凭证和模型 key 混用。
- 一句话总结:签名防篡改、payload 能看不能藏、无状态=验签不查库但不能撤销;短 access+长 refresh 换无感续期;用户 JWT 和模型 key 分层、配额检查在模型调用之前。
本篇速记卡
| 题 | 一句话核心 |
|---|---|
| Q01 async 路由 | async 跑事件循环禁止阻塞、def 自动进线程池;同步库混进 async = 全服务冻结 |
| Q02 Pydantic | 入口拦用户 422、出口拦模型脏输出;BaseSettings 管密钥;瓶颈在模型不在校验 |
| Q03 依赖注入 | 要什么和怎么造分离;依赖树+同请求缓存+鉴权即依赖;yield 管 finally;测试靠 override |
| Q04 SSE | HTTP 单向流精确匹配 LLM 输出;断流只能重生成或转后台;心跳防代理掐连接 |
| Q05 中间件 | 洋葱反序、异常统一靠 handler、trace-id 进 contextvar;流式 body 中间层改不了 |
| Q06 JWT | 签名防篡改不防看、无状态不能撤销;短 access 长 refresh;用户凭证与模型 key 分层 |