模块四 · 工程与运维
认证、缓存与测试:把接口做成产品
能跑通不等于能上线。这一节讲清认证授权的边界、两级缓存的用法,以及单元/集成测试怎么写才有价值。
读完这一节,你能做到
- 区分认证与授权,选对 JWT、Cookie 与 Policy。
- 用 IMemoryCache 与 Redis 做两级缓存,并处理失效。
- 搭出单元测试与集成测试的分层。
- 用 WebApplicationFactory 写一个端到端接口测试。
一、认证与授权
认证解决"你是谁",授权解决"你能做什么"。
| 方式 | 特点 | 适用 |
|---|---|---|
| JWT | 无状态,适合 API 与多端 | 前后端分离 |
| Cookie | 浏览器自动携带,易受 CSRF | 传统 Web |
| OAuth/OIDC | 第三方登录、单点登录 | 企业应用 |
ASP.NET Core 的授权策略:
builder.Services.AddAuthorization(o =>
{
o.AddPolicy("admin", p => p.RequireRole("Admin").RequireClaim("scope", "api"));
});
注意:JWT 的过期时间要短,配合刷新令牌;签名密钥不能硬编码;[Authorize] 只是入口,资源级权限仍要在业务里校验。
二、缓存的两级
| 层级 | 实现 | 特点 |
|---|---|---|
| 本地 | IMemoryCache | 最快,进程内,容量有限 |
| 分布式 | Redis | 跨实例共享,网络开销 |
常见模式:先查本地,再查 Redis,最后查库并回填;失效时先删缓存再更新库,或订阅变更事件。
三个问题:
- 穿透:查不存在的数据,缓存空值或布隆过滤器。
- 击穿:热点 key 过期,加互斥锁或逻辑过期。
- 雪崩:大量 key 同时过期,加随机 TTL。
输出缓存(Output Caching)适合整页/整接口缓存,注意按用户区分的接口不能全局缓存。
三、测试金字塔
单元测试(多)→ 集成测试(中)→ 端到端(少)
- 单元测试:测纯逻辑,不碰数据库与网络,快。
- 集成测试:测管道、数据库、序列化,慢但真实。
- 端到端:测关键路径,最慢,数量最少。
判断标准:一个测试是否值得写,看它能不能在重构时给你信心;只测 getter/setter 没有意义。
四、集成测试
public class ApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ApiTests(WebApplicationFactory<Program> factory)
=> _client = factory.CreateClient();
[Fact]
public async Task Get_health_returns_ok()
{
var res = await _client.GetAsync("/health");
res.EnsureSuccessStatusCode();
}
}
要点:
- 用测试专用数据库(内存库或容器),避免污染开发库。
- 每个测试独立数据,避免顺序依赖。
- 外部依赖用 mock 或 WireMock 替代。
五、质量闸门
- 覆盖率不是目标,关键路径覆盖才有意义。
- 静态分析(nullable、分析器)能提前发现空引用与资源泄漏。
- CI 里跑构建、测试、格式检查,失败即阻断。
- 性能关键路径加基准测试,防止回归。
六、常见坑
- JWT 密钥写进配置文件并提交。
- 只做认证不做资源级授权,越权访问。
- 缓存无 TTL,数据永久不更新。
- 集成测试共用数据库,互相污染。
- 只写单元测试,管道问题到线上才发现。
七、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 配一个 JWT 认证 + 角色策略,写一个越权用例验证。
- 用 IMemoryCache + Redis 做两级缓存,模拟击穿。
- 用 WebApplicationFactory 写三个接口测试:成功、参数错误、未授权。
② 让 AI 写同一题
把第 2 题(认证 + 缓存)交给 AI,补一句 说明 token 校验失败的分支与缓存失效策略。
③ 对比要盯的点
- JWT 校验它验了签名、过期与受众吗?只验签名是不够的。
- 缓存失效是删 key 还是设过期?删 key 时有没有处理并发击穿。
- 测试它用了
WebApplicationFactory还是自己造 mock?前者的可信度高得多。
④ 一句话取舍
token 过期与缓存同时失效的那一秒,它那版会怎样?
八、自测
- 认证与授权的区别?
- JWT 与 Cookie 的适用场景?
- 穿透、击穿、雪崩的方案?
- 测试金字塔的三层?
- 集成测试如何隔离数据?
参考答案要点:身份 vs 权限;无状态 API vs 浏览器;空值/互斥/随机 TTL;单元/集成/端到端;独立库或事务回滚。
附录:高频追问
问:JWT 能主动失效吗? 无状态令牌本身不能,需要黑名单、短过期 + 刷新令牌,或版本号。
问:Token 放哪最安全? HttpOnly Cookie 防 XSS,但要处理 CSRF;localStorage 简单但易被 XSS 读取。
问:Policy 与 Role 的区别? Role 是粗粒度,Policy 可以组合 Claim、角色与自定义要求。
问:缓存一致性怎么保证? 先更新库再删缓存,接受短暂不一致;强一致场景用版本号或锁。
问:IMemoryCache 会过期吗? 可以设绝对/滑动过期;进程重启即丢失。
问:Redis 挂了怎么办? 降级到本地缓存或直接查库,并加限流保护。
问:单元测试要 mock 数据库吗? 纯逻辑不需要;涉及查询翻译的用集成测试更真实。
问:集成测试启动慢怎么办? 复用工厂、共享容器、只测关键路径。
问:覆盖率多少才够? 没有统一标准,核心模块高、边缘代码低;关注未覆盖的分支。
问:如何做接口契约测试? 用 OpenAPI 生成客户端或快照测试,保证前后端契约一致。
附:缓存的边界与降级
缓存不是数据库:强一致场景不能只靠缓存。Redis 故障时要有降级路径:本地缓存、直接查库、限流保护。
缓存键要可观测:把业务前缀、版本号、租户放进去,方便排查与清理。TTL 要有随机抖动,避免同一时刻集体失效。
测试数据的隔离
三种做法:每个测试用独立数据库/容器、每个测试用事务回滚、用唯一前缀生成数据。
避免共享开发库:测试之间会互相影响,失败难以复现。集成测试宁愿慢一点,也要可重复。
补充:JWT 的刷新与撤销
- 访问令牌短期(几分钟到几小时),刷新令牌长期。
- 刷新令牌要能撤销:存服务端或版本号。
- 令牌里不要放敏感信息,它只是签名不是加密。
- 时钟偏移要留余量,否则会出现“刚发就过期”。
补充:授权边界
[Authorize] 只证明“登录了”,不证明“能操作这条数据”。资源级校验必须在业务里做:订单属于当前用户、租户隔离、角色与数据范围。
每个受保护接口至少写一个越权用例,否则权限漏洞很难被发现。
安全验收
至少验证:无令牌返回 401、越权返回 403、过期令牌失效、缓存不泄露他人数据。不要只认证不授权;不要发长期有效令牌;不要缓存敏感数据。
一个判断
接口结果是否因人而异?是,就绝不能做全局输出缓存,要按用户/租户隔离缓存键。
验收
无令牌 401、越权 403、过期失效、缓存不串数据,四项都验过才算完成。先验越权,再上线;缓存键隔离用户。先验越权。
补遗:OAuth2 / OIDC
四个角色:资源所有者、客户端、授权服务器、资源服务器。常见流程:授权码 + PKCE(Web/移动端)、客户端凭证(服务间)。OIDC 在 OAuth2 上加了身份层(ID Token)。
不要用密码模式(已废弃);回调地址要白名单;state/nonce 防 CSRF 与重放。
补遗:常见攻击
SQL 注入(参数化)、XSS(输出编码、CSP)、CSRF(SameSite、Anti-Forgery Token)、越权(资源级校验)、开放重定向(白名单)、敏感数据泄露(加密、脱敏、日志过滤)。
补遗:xUnit、Moq 与 Testcontainers
xUnit 用 [Fact]/[Theory];Moq 模拟接口;Testcontainers 启动真实数据库/Redis 做集成测试,避免“内存库通过、线上失败”。
测试要独立、可重复:每个测试自建数据,外部依赖用容器或 WireMock。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
提示:读完可以顺手做几道对应题目,检验是否真的理解