网络、安全与测试 · 六问6 questions on networking, security and testing
测试的种类、HTTPS vs HTTP、JWT、CORS、session vs cookie、HTTP 状态码。Kinds of tests, HTTPS vs HTTP, JWT, CORS, session vs cookie, HTTP status codes.
这一页有什么On this page7
- 说清 CORS 是谁在拦、预检请求什么时候发、以及为什么前端改不了Explain who blocks a request under CORS, when the preflight request is sent, and why the front end cannot fix it
- 对比 JWT 和 session 在存储位置与失效能力上的根本差别Compare JWT and session on the two points that matter: where the data is stored, and whether you can revoke it
- 分清测试金字塔的三层各测什么Say what each of the three levels of the testing pyramid tests
- 按类别说出常用状态码及其语义Name the common status codes by group, and what each one means
CORS 那道几乎人人遇到过,但能说清「是浏览器在拦、不是服务器拒绝、所以前端改不了」的人不多 —— 这是最有区分度的一道。JWT vs session 会追问「怎么让 JWT 提前失效」,答不出说明只是背过概念。Almost everyone has hit a CORS error, but few can say clearly that the browser is the one blocking it, that the server did not refuse the request, and that the front end therefore cannot fix it. That makes it the question that separates people most. On JWT vs session the follow-up is how to revoke a JWT early, and not having an answer shows you only memorised the definition.
什么是 CORS,怎么解决 CORS 错误What is CORS, and how do you fix a CORS error?
#360 What is CORS and how to solve the CORS error
一句话:浏览器的同源策略默认禁止页面读取 跨源响应;CORS 是服务器通过响应头「授权」某些跨源请求的机制。
三条最关键的认知(这才是区分度):
- 是浏览器在拦,不是服务器拒绝。请求通常已经发出去了、 服务器也已经处理了—— 只是浏览器不让 JS 读响应。所以看到 CORS 错误不等于接口没执行(非幂等接口尤其要注意, 可能已经创建了数据)。
- 所以前端改不了。必须服务端加响应头, 或者走代理。在前端加什么请求头都没用。
- 同源 = 协议 + 域名 + 端口 三者全同。
http和https不同源,3000和3001不同源。
简单请求 vs 预检请求:GET / HEAD /POST 且只用安全头、Content-Type 限于三种 (form-urlencoded、multipart/form-data、text/plain)→ 直接发。
其他情况先发一个OPTIONS 预检。注意 application/json就会触发预检—— 这就是为什么「明明是 POST 却多了一个 OPTIONS 请求」。
四种解法:
- 服务端加头(正解)——
Access-Control-Allow-Origin, Express 里app.use(cors())。 - 开发时用 dev server 代理—— Vite 的
server.proxy, 让浏览器以为是同源。 - 生产用同源部署或网关—— 前端和 API 挂在同一个域下的不同路径。
- (历史方案)JSONP —— 只支持 GET,已淘汰。
会追问:「要带 cookie 怎么办?」—— 前端 credentials: "include", 服务端 Allow-Credentials: true,而且此时Allow-Origin不能是 *,必须写具体域名。 这是最常见的「配了 cors 还是不行」的原因。
「预检能缓存吗?」——Access-Control-Max-Age, 避免每个请求都多一次往返。
In one line: the browser’s same-origin policy stops a page from reading a cross-origin response by default; CORS is the mechanism by which the server uses response headers to authorise some of those requests.
Three things to understand — this is what separates people:
- The browser blocks it; the server did not refuse. The request usually went out and the server usually handled it — the browser just will not let your JS read the response. So a CORS error does not mean the endpoint did not run. Watch out with non-idempotent endpoints: the record may already exist.
- Which is why the front end cannot fix it. The server has to send the headers, or you go through a proxy. No request header you add on the client will help.
- Same origin means scheme, host and port all match.
httpandhttpsare different origins;3000and3001are different origins.
Simple requests vs preflighted ones: GET / HEAD / POST with only safe headers and a Content-Type limited to three values (form-urlencoded, multipart/form-data, text/plain) go straight out.
Anything else sends an OPTIONS preflight first. Note that application/json triggers a preflight — that is why “it is a POST but I see an extra OPTIONS request”.
Four ways to fix it:
- Send the headers from the server (the real fix) —
Access-Control-Allow-Origin, orapp.use(cors())in Express. - Proxy through the dev server while developing — Vite’s
server.proxy, so the browser thinks it is same-origin. - In production, deploy same-origin or put a gateway in front — front end and API on the same domain, different paths.
- (Historical) JSONP — GET only, obsolete.
Follow-up: “What if I need to send cookies?” — credentials: "include" on the client, Allow-Credentials: true on the server, and at that point Allow-Origin cannot be * — it must name the origin. This is the most common reason for “I configured cors and it still does not work”.
“Can the preflight be cached?” — Access-Control-Max-Age, so you do not pay a round trip per request.
HTTPS vs HTTP
#358 HTTPS vs HTTP
一句话:HTTPS = HTTP + TLS 加密层。 同一套协议,只是传输过程被加密和验证了。
它提供三样东西(要说全):
- 加密—— 中间人看不到内容
- 身份验证—— 证书证明「你连的确实是这个域名的服务器」,这一条常被忽略, 但它才是防钓鱼的关键
- 完整性—— 内容被篡改会被发现
握手大致过程:客户端打招呼 → 服务器发证书 → 客户端验证书链 →用非对称加密协商出一个对称密钥 → 之后用对称加密传数据。为什么要混用两种加密?非对称安全但慢, 对称快但要先安全地交换密钥 ——所以用非对称来交换对称密钥。 这一句是加分点。
端口:HTTP 80, HTTPS 443。
会追问:「HTTPS 慢吗?」—— 握手有额外开销, 但TLS 1.3 把握手压到一次往返, 而且HTTP/2 和 HTTP/3 只在 HTTPS 上可用—— 多路复用带来的收益通常超过加密的开销。 所以「用 HTTPS 会变慢」现在基本不成立。
「有了 HTTPS 就安全了吗?」——不。它只保护传输过程。 XSS、SQL 注入、 弱口令、越权 一个都没解决。
In one line: HTTPS is HTTP plus a TLS layer. Same protocol; the transport is now encrypted and authenticated.
It gives you three things — name all three:
- Encryption — someone in the middle cannot read the contents
- Authentication — the certificate proves “you really are talking to the server for this domain”. People forget this one, and it is the part that stops phishing
- Integrity — tampering is detected
Roughly how the handshake goes: client says hello → server sends its certificate → client verifies the chain → they use asymmetric crypto to agree on a symmetric key → everything after that is symmetric. Why mix the two? Asymmetric is secure but slow; symmetric is fast but needs the key exchanged safely first — so you use asymmetric to exchange the symmetric key. That sentence is the bonus point.
Ports: 80 for HTTP, 443 for HTTPS.
Follow-up: “Is HTTPS slow?” — the handshake costs something, but TLS 1.3 gets it down to one round trip, and HTTP/2 and HTTP/3 are only available over HTTPS — the multiplexing usually more than pays for the encryption. So “HTTPS makes it slower” no longer really holds.
“Does HTTPS make me secure?” — no. It protects the transport. XSS, SQL injection, weak passwords and broken authorisation are all still yours to solve.
什么是 JWTWhat is a JWT?
#359 What is JWT
一句话:JSON Web Token —— 一个自带签名的字符串, 服务器不用存它也能验证它没被篡改。
三段结构,用点分隔:header.payload.signature
- header—— 用什么算法签的
- payload—— 用户 id、过期时间等业务数据
- signature—— 对前两段用密钥算出的签名
最重要的一条(必答):前两段只是Base64URL 编码,不是加密——任何人都能解出来看。 所以payload 里绝不能放密码、 身份证号这类敏感信息。 签名保证的是「没被改过」,不是「看不到」。
优点:无状态—— 服务器不用存 session, 天然适合多实例和微服务 (任何一台都能独立验证)。
缺点(这半边是重点):
- 没法主动失效。签出去就有效到过期。 用户改密码、 管理员封号,旧 token 照样能用。
- 体积比 session id 大, 每个请求都要带。
- 放哪都有风险——
localStorage怕 XSS, cookie 怕 CSRF。
会追问:「怎么让 JWT 提前失效?」—— 这是这题的分水岭:
- 短过期 + refresh token—— access token 只活 15 分钟, 用一个可撤销的 refresh token 换新的。这是标准做法。
- 黑名单—— 把要作废的 token id 存 Redis。但这就重新变成有状态了, 等于放弃了 JWT 的主要优点。
安全上还有一个经典坑:验证时必须指定期望的算法, 不能信 header 里写的 —— 否则攻击者把 alg改成 none 就绕过签名了。
In one line: a JSON Web Token is a string that carries its own signature, so the server can verify it has not been tampered with without storing it.
Three parts, separated by dots: header.payload.signature
- header — which algorithm signed it
- payload — the data: user id, expiry and so on
- signature — the first two parts signed with your secret
The one thing you must say: the first two parts are Base64URL encoded, not encrypted — anyone can decode and read them. So never put a password or a national id number in the payload. The signature guarantees “unchanged”, not “unreadable”.
The upside: it is stateless — the server stores no session, which suits many instances and microservices, since any one of them can verify it alone.
The downsides — this half is the real question:
- You cannot revoke it. Once issued it is valid until it expires. User changes their password, an admin bans the account — the old token still works.
- It is bigger than a session id and rides along on every request.
- Every place you store it has a risk —
localStorageis exposed to XSS, a cookie is exposed to CSRF.
Follow-up: “How do you expire a JWT early?” — this is where the question separates people:
- Short expiry plus a refresh token — the access token lives 15 minutes and you trade a revocable refresh token for a new one. This is the standard answer.
- A blocklist — keep revoked token ids in Redis. But now you are stateful again, which gives up the main reason you chose JWT.
One classic security trap: when you verify, you must pin the algorithm you expect rather than trust the one in the header — otherwise an attacker sets alg to none and walks past the signature entirely.
session vs cookie
#361 sessions vs cookies
先纠正一个常见混淆:它们不是同级的东西。cookie 是「浏览器存小数据的机制」,session 是「服务器记住用户状态的方案」—— 而 session 通常靠 cookie 来传那个 id。
| Cookie | Session | |
|---|---|---|
| 存在哪 | 浏览器 | 服务器(内存 / Redis / 数据库) |
| 存什么 | 小字符串(≤ 4 KB) | 任意大小的用户数据 |
| 安全性 | 用户能看能改 | 用户只拿到一个 id |
| 能否主动失效 | 要等过期或被覆盖 | 能,删掉服务端记录就行 |
典型流程:登录成功 → 服务器建 session、 生成 session id → 通过 Set-Cookie 发给浏览器 → 之后每个请求浏览器自动带上 → 服务器用 id 查出用户。
Cookie 的四个安全属性必须会:
HttpOnly—— JS 读不到,防 XSS 偷 cookieSecure—— 只在 HTTPS 下发送SameSite——Strict/Lax/None,防 CSRF的主要手段Max-Age/Domain/Path—— 作用范围
会追问:「session vs JWT 怎么选?」——
- 要能立刻踢人下线(后台管理、支付类)→ session
- 多服务、跨域、 移动端 + Web 共用→ JWT(配 refresh token)
还会问:「session 在多实例部署下怎么办?」—— 存内存会导致「刷新一下就掉登录」 (请求打到别的实例)。 解法是把 session 存 Redis, 或者用粘性会话(不推荐)。
First, clear up the usual confusion: these are not two options at the same level. A cookie is a browser mechanism for storing a small value; a session is a server-side way of remembering who the user is — and a session normally uses a cookie to carry its id.
| Cookie | Session | |
|---|---|---|
| Lives where | The browser | The server (memory / Redis / database) |
| Holds what | A small string, 4 KB or less | User data of any size |
| Security | The user can read it and change it | The user only ever holds an id |
| Can you revoke it? | Only by expiry or overwrite | Yes — delete the server-side record |
The typical flow: login succeeds → the server creates a session and a session id → it goes out via Set-Cookie → the browser attaches it to every subsequent request → the server looks the user up by that id.
You have to know the four cookie security attributes:
HttpOnly— JS cannot read it, which stops XSS from stealing the cookieSecure— only sent over HTTPSSameSite—Strict/Lax/None, the main defence against CSRFMax-Age/Domain/Path— its scope
Follow-up: “Session or JWT?” —
- You need to kick someone out right now (admin panels, anything touching payments) → session
- Many services, cross-domain, one API for both mobile and web → JWT, with a refresh token
They will also ask: “What happens to sessions across several instances?” — keeping them in memory means “refresh the page and I am logged out” when the request lands on a different instance. The fix is to put sessions in Redis, or sticky sessions (not recommended).
常见的 HTTP 状态码What are the common HTTP status codes?
#362 Give some HTTP response status codes
先说五个类别,再举例 —— 这样显得有体系:1xx 信息、2xx 成功、3xx 重定向、4xx 客户端错、5xx 服务端错。
| 码 | 含义 | 什么时候用 |
|---|---|---|
| 200 OK | 成功 | GET / PUT / PATCH 成功 |
| 201 Created | 已创建 | POST 成功,建议带 Location 头 |
| 204 No Content | 成功但没内容 | DELETE 成功 |
| 301 / 302 | 永久 / 临时重定向 | 301 会被浏览器缓存,改错了很难收回 |
| 304 Not Modified | 没变,用缓存 | 配 ETag / Last-Modified |
| 400 Bad Request | 请求有问题 | 参数缺失、格式错、校验失败 |
| 401 Unauthorized | 没登录 / token 无效 | 「你是谁?」 |
| 403 Forbidden | 登录了但没权限 | 「知道你是谁,但你不能干这个」 |
| 404 Not Found | 资源不存在 | |
| 409 Conflict | 冲突 | 重复注册、并发修改 |
| 422 | 语义错误 | 格式对但业务上不合法 |
| 429 | 请求太多 | 限流 |
| 500 | 服务端异常 | 未捕获的错误 |
| 502 / 503 / 504 | 网关错 / 不可用 / 超时 | 上游挂了、在维护、上游太慢 |
401 vs 403 是最常问的一对:401 是「没认证」,403 是「认证了但没授权」。
实践里有个细节:为了不泄露资源是否存在, 有些接口会把「没权限」也返回 404。
会追问:「业务错误该用 4xx 还是 200 带错误码?」——REST 风格用 4xx(让 HTTP 语义承载错误), 但要注意有些老网关会吞掉 4xx 的响应体。GraphQL 则一律返 200, 错误放在 errors 字段里 —— 因为一个请求可能部分成功, 没法用单个状态码表达。这个对比答出来很加分, Federation 那门课里 extensions.code就是干这个的。
Name the five classes first, then give examples — it reads as organised: 1xx informational, 2xx success, 3xx redirect, 4xx client error, 5xx server error.
| Code | Means | When |
|---|---|---|
| 200 OK | Success | A successful GET / PUT / PATCH |
| 201 Created | Created | A successful POST — send a Location header |
| 204 No Content | Success, nothing to return | A successful DELETE |
| 301 / 302 | Permanent / temporary redirect | Browsers cache 301, so a wrong one is hard to take back |
| 304 Not Modified | Unchanged, use your cache | Paired with ETag or Last-Modified |
| 400 Bad Request | The request is wrong | Missing parameter, bad format, failed validation |
| 401 Unauthorized | Not logged in, or the token is invalid | “Who are you?” |
| 403 Forbidden | Logged in but not allowed | “I know who you are, and you cannot do this” |
| 404 Not Found | No such resource | |
| 409 Conflict | Conflict | Duplicate signup, concurrent edit |
| 422 | Semantically wrong | Well-formed but invalid for the business rules |
| 429 | Too many requests | Rate limiting |
| 500 | Server failed | An uncaught error |
| 502 / 503 / 504 | Bad gateway / unavailable / timeout | Upstream is down, in maintenance, or too slow |
401 vs 403 is the pair they ask about most: 401 means not authenticated, 403 means authenticated but not authorised.
One detail from practice: to avoid leaking whether a resource exists, some endpoints return 404 for “not allowed” as well.
Follow-up: “Should a business error be a 4xx or a 200 with an error code?” — REST says 4xx, so the HTTP semantics carry the error, but watch out: some older gateways swallow the body of a 4xx. GraphQL always returns 200 and puts errors in the errors field, because one request can be partially successful and no single status code says that. Drawing that contrast earns you points — in the Federation course, extensions.code is exactly this.
测试有哪几种What kinds of tests are there?
#357 What are the different kinds of tests
一句话:按范围从小到大 ——单元 → 集成 → 端到端, 这就是「测试金字塔」:越往上越慢越脆,所以数量越少。
- 单元测试(unit)—— 测一个函数或一个组件, 依赖全部 mock。快、多、定位准。例:一个纯函数、 一个 React 组件的渲染。
- 集成测试(integration)—— 测几个模块协作是否正确, 可能真的连数据库或起一个测试服务器。 例:调一个 API 端点, 断言它真的写进了库。
- 端到端(E2E)——用真实浏览器走完整用户流程。 Playwright / Cypress。 最接近真实,也最慢最容易随机失败。
还会提到的几种:回归测试(防止改坏老功能)、 快照测试(比对渲染输出,容易变成「随手更新快照」的橡皮章)、 性能 / 压力测试、 可访问性测试、 冒烟测试(上线后快速验证主流程)。
Testing Library 的核心理念值得说:「像用户一样测试」—— 按可见文本和 role 查元素, 而不是按 class 名或组件内部结构。 这样重构内部实现测试不会碎。
会追问:「测试覆盖率要多少?」——不要给一个死数字。 正确回答是:覆盖率只说明「代码被执行过」, 不说明「断言是对的」。
这一点我可以给一个实测例子: Federation 那门课的源项目里, 六个端点全部 return null也能通过 3 个测试, node-subgraph 的 4 个「通过」里3 个是「空实现恰好满足断言」。所以「测试通过 ≠ 做对了」—— 比覆盖率数字更该关心的是断言够不够强。
In one line: smallest scope to largest — unit → integration → end-to-end. That is the testing pyramid: higher means slower and flakier, so you write fewer of them.
- Unit — one function or one component, everything else mocked. Fast, numerous, and precise about where the problem is. A pure function; a React component rendering.
- Integration — do a few modules work together, possibly against a real database or a test server. Call an API endpoint and assert the row really landed.
- End-to-end — a real browser walking a whole user journey. Playwright or Cypress. Closest to reality, and also the slowest and the most prone to random failure.
Others worth mentioning: regression tests (so old behaviour does not break), snapshot tests (comparing rendered output — which easily degrades into rubber-stamping “update snapshot”), performance and load tests, accessibility tests, and smoke tests for a quick check of the main flow after a deploy.
Testing Library’s core idea is worth stating: “test it the way a user uses it” — find elements by visible text and role, not by class name or internal component structure. Then refactoring the internals does not shatter the tests.
Follow-up: “What coverage number should you aim for?” — do not give a number. The right answer is that coverage tells you code was executed, not that the assertions are any good.
Here is a measured example: in the source project behind the Federation course, six endpoints that all just return null still passed 3 tests, and of the 4 passes in node-subgraph, 3 were an empty implementation happening to satisfy the assertion. So a green test does not mean you got it right — the strength of the assertions matters more than the coverage percentage.
换一道题也能用Works on other problems too
考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.
- CORS 是浏览器在拦,请求可能已经执行了;前端无解,靠服务端加头或代理。CORS is the browser blocking the response, and the request may already have run. The front end cannot fix it; the server has to send the header, or you use a proxy.
- application/json 会触发 OPTIONS 预检;带 cookie 时 Allow-Origin 必须写具体域名。application/json triggers an OPTIONS preflight; when a cookie is sent, Allow-Origin has to name the exact origin.
- HTTPS 给三样:加密、身份验证、完整性;非对称交换密钥、对称传数据。HTTPS gives you three things: encryption, proof of identity, and integrity. Keys are exchanged with asymmetric cryptography, then data is sent with symmetric.
- JWT 的 payload 只是 Base64 不是加密;最大缺点是没法主动失效,标准解法是短过期 + refresh token。The payload of a JWT is only Base64, not encrypted. Its biggest weakness is that you cannot revoke it, and the standard answer is a short expiry plus a refresh token.
- cookie 是浏览器存储机制,session 是服务端状态方案,后者靠前者传 id;四个安全属性要会。A cookie is browser storage, a session is a server-side approach to state, and the session uses the cookie to carry its id. Know the four security attributes.
- 401 没认证、403 没授权;201 创建、204 删除;GraphQL 一律 200 把错误放 errors。401 means not signed in, 403 means signed in but not allowed; 201 for created, 204 for deleted; GraphQL always returns 200 and puts problems in errors.
- 测试金字塔单元→集成→E2E;覆盖率不代表断言强 ——「空实现恰好通过」是实测过的。The testing pyramid goes unit, then integration, then end-to-end. Coverage does not measure how strong the assertions are: an empty function passing the test has really happened.