面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月29日 00:24

Cypress 中 Page Object 模式有必要用吗?

Page Object 模式将页面元素选择器和操作封装为独立类,测试代码只调用方法不直接写选择器,页面变更时只需改 Page Object 不改测试。但在 Cypress 中,Custom Command 常能替代 POM 的大部分功能——cy.login() 比 loginPage.login() 更符合 Cypress 风格。POM 真正有价值的场景是:多页面复杂流程(如电商下单流程跨 4 个页面)、团队已熟悉 POM 模式、选择器需要跨多个测试文件共享复用。追问Cypress 官方对 POM 的态度是什么?官方认为 POM 不是必须的,Cypress 的 Custom Command 和组合式 API 已能很好复用逻辑;过度封装反而增加维护成本,简单场景用 Custom Command 更合适。Custom Command 和 POM 怎么选?单页面或少交互场景用 Custom Command(如 cy.login());多页面流程且团队习惯 OOP 风格时用 POM,两者可混合使用。POM 中选择器应该怎么管理?统一使用 data-testid 属性作为选择器锚点,不依赖 CSS class 或 DOM 结构,UI 样式变更不影响测试稳定性。POM 类变得臃肿怎么办?拆分为基础 PageObject(通用方法)+ 具体页面子类;组件级别的对象(如导航栏)独立为 Component Object,避免单类膨胀。写段代码// POM 类 + 测试使用class LoginPage { get username() { return cy.get('[data-testid=username]'); } get password() { return cy.get('[data-testid=password]'); } login(user, pass) { this.username.type(user); this.password.type(pass); cy.get('[data-testid=submit]').click(); }}// 测试中const login = new LoginPage();login.login('admin', '123456');cy.url().should('include', '/dashboard');
前端阅读 05月29日 00:24

FFmpeg的核心组件包括哪些?分别有什么作用?

FFmpeg的核心组件分为库和命令行工具两大类。库层面:libavcodec负责音视频编解码(H.264/H.265/AAC等),libavformat处理容器封装与解封装(MP4/MKV/FLV),libavfilter实现滤镜链(缩放/旋转/叠加),libswscale做像素格式转换(YUV↔RGB),libswresample处理音频重采样与通道转换,libavutil提供通用数据结构(AVPacket/AVFrame)和工具函数,libavdevice抽象硬件设备交互。工具层面:ffmpeg是转码命令行入口,ffprobe探测媒体流信息,ffplay轻量播放器。转码流水线为:demux(libavformat)→ decode(libavcodec)→ filter(libavfilter)→ encode(libavcodec)→ mux(libavformat)。追问libavcodec和libavformat的职责边界在哪?为什么要把编解码和容器处理拆成两个库?转码时用-c copy跳过了流水线中哪些环节?为什么能实现无损且秒级完成?libswscale和libavfilter的scale滤镜功能重叠,实际项目中该用哪个?ffprobe如何快速获取视频时长和码率?底层调用了libavformat的哪个接口?硬件加速编解码(NVENC/QSV)在组件架构中如何接入?是否绕过了libavcodec?写段代码# 查看视频流信息ffprobe -v quiet -print_format json -show_streams input.mp4# H.264转H.265,音频直接拷贝ffmpeg -i input.mp4 -c:v libx265 -crf 28 -c:a copy output.mp4
服务端阅读 05月29日 00:24

如何优化 Cypress 测试的执行速度?

核心优化手段:用 cy.session() 缓存登录状态避免重复登录;通过 --parallel 并行执行拆分 spec 文件;用 cy.intercept() 拦截 mock 网络请求减少真实 API 调用;避免 cy.wait() 硬编码等待,让 Cypress 自动重试机制生效;配置 baseUrl 避免重复导航。综合使用可将 1000+ 用例执行时间从 20 分钟压到 5 分钟以内。追问cy.session() 和 before() 中登录有什么区别?before() 每个测试文件都会执行登录;cy.session() 在同文件内跨测试复用登录状态,且 session 失效时自动重建,减少冗余请求。并行执行为什么需要 Cypress Cloud?Cypress 的并行调度依赖 Dashboard 服务分配测试到不同机器,免费版可用 cypress-parallel 插件做本地并行,但缺少负载均衡。如何识别最慢的测试用例?运行 cypress run --reporter=json 生成报告,按 duration 排序定位瓶颈;或在 Cypress Cloud 查看耗时分布图。spec 文件应该怎么拆分?按功能模块拆分,每个 spec 控制在 10-20 个测试;避免单文件过大影响并行均衡,也避免过碎导致启动开销占比过高。cy.intercept() mock 数据会不会导致测试失真?会,应在关键流程用真实 API,仅在辅助请求(如第三方服务)使用 mock,并在 CI 中定期跑无 mock 的全量回归验证。写段代码// cy.session 缓存登录 + intercept mockbeforeEach(() => { cy.session('user', () => { cy.intercept('POST', '/api/login', { statusCode: 200 }); cy.visit('/login'); cy.get('[name=email]').type('user@test.com'); cy.get('[name=password]').type('pass123'); cy.get('button').click(); });});
服务端阅读 05月29日 00:24

Cypress 测试隔离和数据管理怎么做?

Cypress 默认每个 it 块前会重置浏览器状态(清 cookie、localStorage、sessionStorage),Cypress 12+ 开启 testIsolation: true 后更强——每次测试前自动 cy.visit() 恢复初始页面。数据管理分三层:fixtures 管理静态数据、cy.request() + 自定义命令做动态数据创建、cy.task() 操作数据库清理。核心原则:测试不依赖执行顺序,每个测试自给自足。追问cy.session() 怎么用?和 beforeEach 中登录有什么区别?cy.session() 缓存登录后的 cookie 和 localStorage,同一 spec 内重复使用时不重新登录,显著加速测试。而 beforeEach 每次都执行完整登录流程。session 在 spec 间不共享(Cypress 12+ 的限制),跨 spec 需配合 cy.request 预置状态。testIsolation: true 和 false 各适合什么场景?true(默认)适合功能测试,保证每个用例干净状态;false 适合需要跨测试保持状态的端到端流程测试(如多步向导),但需手动在 beforeEach 中清理关键状态。fixtures 和 cy.task() 生成数据怎么选?fixtures 适合不变的测试输入(表单数据、API 响应模板);cy.task() 适合需要与后端交互的动态数据(创建测试用户、插入数据库记录),task 在 Node 环境执行,能直连数据库。如何保证并行执行时测试数据不冲突?用唯一标识生成数据:Date.now() 或 Cypress._.random(),避免固定用户名。测试结束在 afterEach 中通过 cy.request 或 cy.task 清理自己创建的数据,不依赖全局 reset。写段代码// cy.session 加速登录 + fixtures 管理数据Cypress.Commands.add('login', (role) => { cy.session(role, () => { cy.fixture('users').then((u) => { cy.request('POST', '/api/login', u[role]); }); });});
服务端阅读 05月29日 00:24

JWT 如何满足 GDPR 等合规性要求?

JWT 与合规的核心矛盾:JWT 不可变(签发后不能修改内容),但 GDPR 要求用户可以删除数据(被遗忘权)。解决思路有三个:一是 payload 只放最小必要信息(用户 ID + 角色),不存个人数据;二是实现 token 黑名单或短期 TTL,用户请求删除时立即作废所有 token;三是审计日志记录所有 token 签发和使用,满足可追溯要求。对 HIPAA/PCI DSS 同理——JWT 中绝不放 PHI(受保护健康信息)和支付卡号,只存引用 ID。## 追问GDPR 被遗忘权与 JWT 不可变性怎么协调?JWT 内容无法修改,但可以通过作废来"遗忘":将用户所有 token 加入黑名单、使 refresh token 失效、删除服务端用户数据。过期 token 中的个人信息残留属于技术限制,GDPR 在合理范围内允许。HIPAA 对 JWT 有什么特殊要求?不能在 payload 中存储任何 PHI(诊断、药物、病历等),只放用户 ID 和 scope 声明。所有 PHI 访问必须记录审计日志(谁在什么时间访问了什么数据),JWT 中的 sub 和 scope 用于日志关联。密钥轮换与合规审计有什么关系?合规框架(SOC 2/ISO 27001)要求定期轮换密钥。实现方式:维护多个密钥版本,新 token 用新密钥签发,验证时按 kid 字段选择对应密钥。轮换期间旧 token 逐步过期,无需强制用户重新登录。JWT 的审计日志应该记录什么?至少包含:用户 ID、操作类型(签发/验证/撤销)、时间戳、来源 IP、User-Agent、token 前缀(不记录完整 token)。日志存储到不可篡改的系统(如 append-only 存储),满足 SOC 2 审计要求。## 写段代码javascript// GDPR 合规:数据删除 + token 作废async function deleteUserData(userId) { // 作废所有 token await redis.set(`deleted:${userId}`, '1', 'EX', maxTokenTTL); // 删除用户数据 await db.delete('users', { id: userId }); // 审计日志 await auditLog('DATA_DELETED', { userId, reason: 'user_request' });}// 验证时检查用户是否已删除if (await redis.get(`deleted:${decoded.sub}`)) { throw new Error('user data deleted');}
服务端阅读 05月29日 00:24

Cypress 和 Selenium 有什么核心区别?

Cypress 直接运行在浏览器内部,通过 Chrome DevTools API 与页面通信,无需 WebDriver 中间层;Selenium 通过外部 WebDriver 进程以 HTTP 协议控制浏览器,架构上多了一跳延迟。Cypress 自动重试断言、内置时间旅行调试、仅支持 Chromium 系浏览器;Selenium 支持所有主流浏览器但需手动处理显式等待。选择依据:前端 SPA 项目选 Cypress 快速反馈,跨浏览器兼容测试选 Selenium。追问Cypress 的自动等待机制和 Selenium 的显式等待有什么区别?Cypress 在断言失败时自动重试(默认 4 秒),无需手动写 wait;Selenium 必须用 WebDriverWait 显式等待元素出现,否则直接抛异常。Cypress 为什么不支持跨域和多标签页?Cypress 运行在同源策略下,跨域需 cy.origin() 处理,多标签页通过模拟而非真正打开新窗口,这是架构上的硬限制。Selenium Grid 和 Cypress Cloud 的并行策略有何不同?Selenium Grid 自建节点分发测试到不同浏览器,免费可控;Cypress Cloud 依赖官方服务按机器数并行,免费版有限制。两者在 CI/CD 中如何选择?小团队前端项目用 Cypress 开发体验好、上手快;大型项目需 Firefox/Safari 兼容性验证时,Selenium 更合适,也可混合使用。写段代码// Cypress: 自动等待,无需显式 waitcy.get('#username').type('user');cy.get('#password').type('pass');cy.get('button').click();cy.url().should('include', '/dashboard');// Selenium: 必须显式等待WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, 'username')));
服务端阅读 05月29日 00:24

Cypress 视觉回归测试怎么做?

Cypress 本身不内置视觉回归功能,需借助插件实现:cypress-image-diff(轻量免费)、Percy/Chromatic(云端对比平台,付费)。核心流程是首次运行生成基准截图,后续运行做像素级 diff,差异超过阈值则判定失败。关键配置是 threshold 容忍度和动态内容排除策略。追问cypress-image-diff 和 Percy 怎么选?cypress-image-diff 本地运行、零费用、适合小团队,截图存仓库;Percy 提供云端可视化审阅、多浏览器快照、PR 集成审批流,适合中大型团队。Percy 还能自动处理抗锯齿和字体渲染差异。动态内容(日期、轮播图)导致误报怎么处理?三种策略:1) 截图前用 CSS 隐藏动态区域 cy.get('.carousel').invoke('css', 'visibility', 'hidden');2) 插件的 ignore 区域配置;3) 用 cy.clock() 冻结时间,使时间戳固定。threshold 阈值怎么设定?像素级对比用 0.01-0.05(严格),感知对比用 0.1-0.2(宽松)。建议核心页面 0.01,次要页面 0.1。首次跑测试建立 baseline 后再微调。CI 环境中截图不一致怎么解决?CI 和本地渲染差异(字体、GPU)导致误报。方案:1) Docker 统一运行环境;2) 只在 CI 中做视觉测试;3) 用 Percy 等云端工具消除本地差异;4) 禁用动画和字体反锯齿。写段代码// cypress-image-diff 基本用法cy.compareSnapshot('homepage', 0.02);// 排除动态区域cy.get('.live-data').invoke('css', 'visibility', 'hidden');cy.compareSnapshot('dashboard', 0.05);
服务端阅读 05月29日 00:23

JWT 在生产环境有哪些常见问题?怎么解决?

JWT 生产环境最核心的问题是三个:token 无法主动撤销(签发后到过期前一直有效)、payload 明文可读(Base64 非加密,敏感信息暴露)、token 体积大影响性能(每次请求都要传 1-2KB)。对应解法:撤销用黑名单(Redis 存已注销 token,TTL 等于剩余过期时间)或短期 access token + refresh token 轮换;敏感数据只放引用 ID,详情查库;体积优化用短字段名和 ES256 算法(比 RS256 小约 50%)。## 追问token 泄露了怎么办?XSS 可窃取 localStorage 中的 token,CSRF 可利用 Cookie 中的 token。防护:优先用 HttpOnly + SameSite Cookie 存储,配合 CSRF Token;access token 设短过期时间(15 分钟),泄露窗口小;检测到异常时强制刷新 refresh token 使旧 access token 自然失效。Refresh Token 轮换是什么?每次用 refresh token 换新 access token 时,同时签发新 refresh token 并作废旧的那个。如果检测到旧 refresh token 被复用,说明泄露,立即撤销该用户所有 refresh token,强制重新登录。JWT 验签每次都要算 HMAC,性能怎么优化?对称算法验签很快(微秒级),一般不是瓶颈。非对称算法(RS256)较慢,可切换到 ES256。大规模场景可在 API 网关层做验签,避免业务服务重复计算。多设备登录怎么管理?每个设备签发独立 refresh token,服务端记录设备信息。踢下线时删除对应 refresh token,该设备 access token 过期后无法续期,自然登出。## 写段代码javascript// 黑名单撤销 + refresh token 轮换async function logout(token) { const { exp } = jwt.decode(token); await redis.setex(`bl:${token}`, exp - now(), '1');}async function refresh(oldRt) { if (await redis.get(`rt:${oldRt}`)) { await redis.del(`rt:${oldRt}`); const newRt = crypto.randomBytes(40).toString('hex'); await redis.setex(`rt:${newRt}`, 7*86400, userId); return { accessToken, refreshToken: newRt }; } throw new Error('invalid refresh token');}
服务端阅读 05月29日 00:23

Cypress 要不要用 Page Object Model?

Cypress 官方并不推荐传统 POM——自定义命令和 App Actions 模式比 POM 更契合 Cypress 的命令链机制。POM 把 DOM 选择器封进类方法,但 Cypress 的重试机制使得类方法中返回 this 的链式调用容易丢失重试上下文。真正需要时,可用轻量页面对象仅封装选择器常量,操作逻辑仍交给自定义命令。追问Cypress 官方推荐的替代模式是什么?App Actions:通过 cy.request() 直接调用 API 设置状态,跳过 UI 操作步骤。例如登录不再走页面填写表单,而是 cy.request('POST', '/api/login', credentials) 配合 cy.session() 缓存。POM 在什么场景下仍有价值?页面交互极其复杂、选择器频繁变更的 SPA 项目中,POM 的选择器集中管理仍有意义。但应避免在 POM 方法中使用 Cypress 命令,改为返回选择器字符串供测试中组合。POM 方法中 cy.get() 返回 this 为什么有问题?cy.get() 返回 Chainable 而非页面对象实例,在 POM 方法中 return this 会导致后续 .should() 等断言脱离 Cypress 重试队列。正确做法是方法内完成全部操作,不返回 this 继续链式调用。选择器管理有没有更好的方案?用 data-cy 或 data-testid 属性统一选择器策略,配合自定义命令封装常用操作,比 POM 类更轻量且不丢失重试能力。写段代码// 推荐:选择器常量 + 自定义命令const selectors = { email: '[data-cy=email]', submit: '[data-cy=submit]' };Cypress.Commands.add('login', (email, pwd) => { cy.get(selectors.email).type(email); cy.get(selectors.submit).click();});
服务端阅读 05月29日 00:23

JWT 有哪些安全风险?如何防护?

JWT 的 payload 仅做 Base64 编码而非加密,任何持有 token 的人都能解码查看内容;存储在 localStorage 中的 token 易遭 XSS 窃取;攻击者可将算法篡改为 none 绕过签名验证。核心防护策略:使用短期 Access Token + 长期 Refresh Token 机制;token 存入 HttpOnly Cookie 并设置 SameSite 防 CSRF;服务端固定签名算法拒绝 alg:none;敏感数据使用 JWE 加密传输。追问JWT 无法主动撤销怎么办?设置 15-30 分钟短过期时间,配合 Redis 黑名单或 token 版本号机制,关键操作强制二次验证。HttpOnly Cookie 和 localStorage 存 token 各有什么利弊?HttpOnly 防 XSS 但需额外防 CSRF(SameSite 属性);localStorage 不防 XSS 但天然免疫 CSRF,取舍取决于项目威胁模型。RS256 和 HS256 该选哪个?RS256 非对称签名,公钥验签私钥签发,适合分布式系统;HS256 对称签名密钥需共享,密钥泄露风险更高,优先选 RS256。如何防止 token 重放攻击?在 payload 中加 jti(唯一 ID)声明,服务端记录已用 jti 拒绝重复请求,配合短过期时间降低窗口期。写段代码// 短期 Access Token + Refresh Token 签发const accessToken = jwt.sign( { sub: userId, jti: uuid() }, RSA_PRIVATE_KEY, { algorithm: 'RS256', expiresIn: '15m' });const refreshToken = jwt.sign( { sub: userId, type: 'refresh' }, REFRESH_SECRET, { expiresIn: '7d' });
服务端阅读 05月29日 00:23

JWT 和 OAuth2.0 是什么关系?有什么区别?

它们不在同一层面:JWT 是令牌格式标准(RFC 7519),定义 token 的结构和编码方式;OAuth2.0 是授权框架(RFC 6749),定义授权流程和角色关系。一句话:OAuth2.0 规定了"怎么授权",JWT 规定了"令牌长什么样"。两者可以组合——OAuth2.0 的 Access Token 可以采用 JWT 格式,但不是必须的。OAuth2.0 也可以用不透明的随机字符串做 token。## 追问OAuth2.0 不用 JWT 行不行?完全可以。OAuth2.0 对 token 格式没有约束,可以用随机字符串(opaque token),验证时需查库。用 JWT 的好处是资源服务器可以本地验签、无需回调授权服务器,降低延迟和耦合。OAuth2.0 的四种授权模式分别适合什么场景?授权码模式(Authorization Code)适合有后端的 Web 应用,最安全;客户端凭证模式(Client Credentials)适合服务间调用;资源所有者密码模式已不推荐;隐式模式(Implicit)因安全风险已被 OAuth2.1 移除,改用 PKCE 增强的授权码模式。既然 JWT 可以独立做认证,为什么还需要 OAuth2.0?JWT 解决的是单系统的 token 格式问题。OAuth2.0 解决的是第三方委托授权问题——"让用户授权 A 应用访问 B 服务的资源,且不暴露密码"。这是 JWT 独自做不到的。OAuth2.0 用 JWT 做 token 时,刷新令牌也用 JWT 吗?一般不用。Refresh Token 需要可撤销,通常用 opaque token 存在服务端。JWT 格式的 refresh token 撤销困难,违背 refresh token 的设计初衷。## 写段代码javascript// OAuth2.0 授权码流程中签发 JWT access tokenconst accessToken = jwt.sign( { sub: userId, scope: 'read write', client_id: appId }, PRIVATE_KEY, { algorithm: 'RS256', expiresIn: '1h' });const refreshToken = crypto.randomBytes(32).toString('hex');await redis.set(`rt:${refreshToken}`, userId, 'EX', 7*86400);
服务端阅读 05月29日 00:23

Cypress 怎么拦截和模拟网络请求?

用 cy.intercept() 拦截匹配规则的 HTTP 请求,配合 .as() 别名和 cy.wait('@alias') 实现请求等待与断言。intercept 可返回固定 stub 响应、动态构造响应、修改请求头或延迟响应,让测试脱离真实 API 依赖。注意 intercept 必须在请求发起前注册,否则无法捕获。追问cy.intercept() 和已废弃的 cy.route() 有什么区别?route 只能拦截 XMLHttpRequest,intercept 同时支持 XHR 和 Fetch API。intercept 使用 RouteMatcher 对象匹配请求(支持 method、url、headers 等多维度),功能远超 route。Cypress 6+ 已弃用 route。怎么 stub 一个带动态参数的请求?用函数式 handler:cy.intercept('GET', '/api/users*', (req) => { req.reply({ body: mockData[req.query.page] }); }),根据 req.query 或 req.body 动态构造响应。如何模拟网络错误和超时?cy.intercept('GET', '/api/data', { forceNetworkError: true }) 强制网络错误;cy.intercept('GET', '/api/data', { delay: 3000, statusCode: 200, body: {} }) 模拟超时或慢响应。多个 intercept 匹配同一请求时哪个生效?按注册顺序,最后注册的优先。建议用精确匹配规则(url + method + headers)避免冲突,或用 .as() 明确指定等待哪个。写段代码cy.intercept('GET', '/api/users*', (req) => { req.reply({ statusCode: 200, body: { users: [] } });}).as('getUsers');cy.visit('/users');cy.wait('@getUsers').its('response.statusCode').should('eq', 200);
服务端阅读 05月29日 00:23

什么是 JWT?它由哪三部分组成?

JWT(JSON Web Token,RFC 7519)是一种紧凑的自包含令牌格式,用于在各方之间安全传输信息。它由三部分组成,用点号分隔:Header.Payload.Signature。Header 指定令牌类型和签名算法(如 HS256);Payload 承载声明(Claims),包括注册声明(iss/exp/sub)、公共声明和私有声明;Signature 用 Header 指定的算法对前两部分签名,保证完整性。注意 Payload 只是 Base64 编码而非加密,不能放敏感数据。## 追问JWT 的 Signature 是怎么生成的?将 Base64Url 编码的 Header 和 Payload 用点号拼接,再用 Header 中声明的算法和密钥进行签名。公式:HMACSHA256(base64(header) + '.' + base64(payload), secret)。接收方用同一密钥重新计算并比对,即可判断 token 是否被篡改。注册声明中的 exp、iat、nbf 各是什么?exp(Expiration Time)过期时间,iat(Issued At)签发时间,nbf(Not Before)生效时间。服务端验证时会检查这些字段,过期的 token 直接拒绝。HS256 和 RS256 有什么区别?HS256 是对称加密,签发和验证用同一个密钥,适合单服务场景;RS256 是非对称加密,私钥签发、公钥验证,适合微服务架构——各服务只需公钥即可验签,无需暴露私钥。JWT 和 JWE 有什么关系?JWT 只保证完整性(防篡改),不保证机密性。JWE(JSON Web Encryption)在 JWT 基础上加密 payload,实现机密性。需要保护敏感数据时用 JWE,一般场景 JWT 足够。## 写段代码javascript// 签发与验证const token = jwt.sign( { sub: 'u123', role: 'admin', iat: Math.floor(Date.now()/1000) }, SECRET, { expiresIn: '1h', algorithm: 'HS256' });const decoded = jwt.verify(token, SECRET);
服务端阅读 05月29日 00:23

Cypress 自定义命令怎么创建和复用?

通过 Cypress.Commands.add() 在 cypress/support/commands.js 中注册自定义命令,将重复操作封装为可链式调用的 cy.xxx() 方法。自定义命令返回 cy 对象,与内置命令行为一致,支持重试和超时机制。定义时注意命名唯一、避免与内置命令冲突,复杂逻辑优先用普通工具函数而非自定义命令。追问Cypress.Commands.add() 的第二个参数支持哪些选项?可传入配置对象 { prevSubject: 'element' } 使命令接收前一个命令的 subject,实现类似 cy.get('input').myCommand() 的链式用法。prevSubject 可选 'optional'、'required'、'noop' 等。自定义命令和普通工具函数怎么选?需要重试、超时、命令日志或链式调用时用自定义命令;纯数据处理、复杂条件逻辑用普通函数。过度使用自定义命令会导致命令日志噪音和调试困难。如何覆盖已有命令?用 Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 扩展内置命令行为,如自动添加认证 header。慎用,会全局影响。自定义命令中如何正确处理异步?命令内部必须使用 Cypress 命令(cy.get、cy.request 等)而非原生 Promise,否则无法重试。如需返回值,用 .then() 提取。写段代码// cypress/support/commands.jsCypress.Commands.add('login', (email, password) => { cy.session([email, password], () => { cy.request('POST', '/api/login', { email, password }); });});// 测试中使用cy.login('user@test.com', 'pass123');
服务端阅读 05月29日 00:23

JWT 和 Session 认证有什么区别?各适合什么场景?

核心区别在于状态存储位置:JWT 是无状态的,用户信息编码在 token 中由客户端持有;Session 是有状态的,用户信息存在服务端,客户端只拿一个 Session ID。这意味着 JWT 天然支持水平扩展(任何节点都能验签),但一旦签发就无法主动撤销;Session 可以随时销毁,但多节点需要共享存储(如 Redis)。选择依据:分布式系统、移动端、跨域 API 选 JWT;需要即时吊销权限、传统 Web 应用选 Session。## 追问JWT 无法主动撤销,用户登出怎么办?常用三种方案:黑名单(Redis 存已撤销 token,过期自动清除)、短期 access token + refresh token 轮换、token 版本号(用户表加 version 字段,签发时写入,验证时比对)。JWT 存 localStorage 还是 Cookie?各有利弊。localStorage 容易被 XSS 窃取;HttpOnly Cookie 防 XSS 但需额外防 CSRF。生产推荐 HttpOnly + SameSite Cookie + CSRF Token 双重防护。Session 在微服务下怎么共享?用 Redis 集群做集中式 Session 存储,或用 Spring Session 等框架透明化处理。本质是把有状态存储从本地内存移到分布式缓存。JWT 的 payload 能放敏感信息吗?不能。Payload 只是 Base64 编码,不是加密,任何人都能解码。敏感数据只存引用 ID,详情查库获取。## 写段代码javascript// 短期 access token + 长期 refresh tokenconst accessToken = jwt.sign( { sub: userId, role }, SECRET, { expiresIn: '15m' });const refreshToken = crypto.randomBytes(40).toString('hex');await redis.setex(`refresh:${userId}`, 7*86400, refreshToken);
计算机基础阅读 05月29日 00:12

TCP 可靠传输靠哪些机制保证?RTO 和 SACK 是怎么工作的?

TCP 可靠传输靠六个机制保证:序列号+确认应答(保证有序、检测丢包)、超时重传+快重传(丢包恢复)、校验和(检测数据损坏)、流量控制(防接收方溢出)、拥塞控制(防网络过载)。其中序列号和重传是核心,其他都是辅助。追问超时重传的 RTO 怎么算?为什么不能固定值?网络延迟波动大,固定值不靠谱——设小了正常延迟就触发多余重传,设大了真丢包要等很久才恢复。RTO 基于实测 RTT 动态计算:RTO = SRTT + 4 × RTTVAR,SRTT 是平滑 RTT(历史加权平均),RTTVAR 是 RTT 波动幅度。首次测量 SRTT = RTT,后续每次 SRTT = 7/8 × 旧SRTT + 1/8 × 新RTT。Linux 还有个下限(tcprtomin,默认 200ms),防止 RTO 过小导致过度重传。累积确认有什么问题?SACK 怎么解决的?累积确认只告诉你"到这个序号之前的都收到了",中间丢了哪个说不清。比如发了 1-10 号包,5 号丢了,ACK 会一直回 5(表示期望收到 5),6-10 号收到了但不确认。发送方不知道 6-10 号到底到了没有,可能重传 5-10 全部。SACK(Selective ACK)在 TCP 选项里额外报告已收到的非连续块,发送方就知道只重传 5 号就行。Linux 默认开启 SACK(tcp_sack=1),对高延迟高丢包链路很重要。序列号为什么是按字节编号而不是按包编号?因为 TCP 是字节流协议,应用层不关心包的边界——你写 1000 字节,TCP 可能拆成两个 500 字节的段,也可能合成一个。按字节编号让接收方能精确知道每个字节的位置,不管怎么拆分合成都能正确重组和确认。也方便滑动窗口按字节数控制流量。面试追问:ISN(初始序列号)为什么不用 0 或 1?防止旧连接的报文段被新连接误接收——ISN 基于时钟递增,每隔 4 微秒加 1,不同连接的序列号空间错开。校验和能检测所有错误吗?不能。校验和是 16 位反码求和,只能检测单比特错误和大部分多比特错误,但两个错误恰好互相抵消时校验和不变。以太网的 CRC 能检测更多错误,但端到端路径上中间设备可能修改数据(如 NAT 改 IP 地址),TCP 校验和是端到端验证的最后防线。如果需要更强的完整性保证,要用 TLS 的 MAC 或应用层哈希。写段代码# 模拟 TCP 序列号和累积确认def simulate_seq(): sent = 0 # 下一个要发的字节号(ISN=0 简化) acked = 0 # 已确认的字节号 segments = [(0, 100), (100, 200), (200, 300)] # (起始, 结束) for start, end in segments: print(f"发送: seq={start}, 数据长度={end-start}") # 模拟中间段丢失 print(f"收到: ACK={100} (第1段确认)") print(f"收到: ACK={100} (第2段丢失,重复ACK)") print(f"收到: ACK={100} (第3个重复ACK→快重传)") print(f"重传: seq={100}, 数据长度=100") print(f"收到: ACK={300} (累积确认到第3段)")simulate_seq()
计算机基础阅读 05月29日 00:11

TCP 四次挥手的过程是什么?为什么不能三次挥手?

TCP 四次挥手是断开连接的过程:主动方发 FIN,被动方回 ACK,被动方再发 FIN,主动方回 ACK。之所以要四次而不是三次,是因为 TCP 全双工——每个方向必须单独关闭。第二次挥手时被动方可能还有数据没发完,不能把 FIN 和 ACK 合并,只能先确认,等数据发完再关自己的方向。追问TIME_WAIT 为什么要等 2MSL?两个原因。第一,确保最后的 ACK 能到——如果 ACK 丢了,被动方会重传 FIN,TIME_WAIT 状态的主动方可以重新回 ACK。如果主动方直接 CLOSED,收到重传的 FIN 只能回 RST,被动方会报连接异常。第二,让网络中属于这个连接的旧报文段全部消亡——MSL 是报文最大生存时间(RFC 793 建议 2 分钟,Linux 实际是 60 秒),等 2MSL 确保来回两个方向的旧包都过期,不会干扰后续新连接。TIME_WAIT 过多怎么办?短连接场景下(比如 HTTP/1.0 每个请求一个连接),主动关闭方会积累大量 TIMEWAIT,每个占一个五元组(源IP、源端口、目的IP、目的端口、协议),端口耗尽后新连接无法建立。解决方法:启用 tcp_tw_reuse(允许 TIMEWAIT 端口给新连接用,依赖时间戳判断旧包)、改用长连接(HTTP/1.1 Keep-Alive)、让客户端主动关闭(服务端被动关闭不产生 TIME_WAIT)。注意 tcp_tw_recycle 在 NAT 环境下会导致连接失败,Linux 4.12 后已移除此选项。为什么不能三次挥手?假设第二次和第三次合并——被动方收到 FIN 后立刻发 FIN+ACK。问题是被动方可能还有数据没发完:它收到主动方的 FIN 只是说"我不发了",不代表"你也不能发了"。被动方需要继续发送剩余数据,发完才能关自己的方向。如果提前发 FIN,这些数据就丢了。只有在被动方恰好也没有数据要发时,才能合并成三次挥手——这就是延迟 ACK 场景下偶尔观察到三次挥手的原因。CLOSE_WAIT 很多是什么问题?CLOSEWAIT 是被动方收到 FIN 后、还没发 FIN 之前的状态。大量 CLOSEWAIT 说明应用层没有调用 close()——通常是因为代码 bug:忘了关 socket,或者线程池满了来不及处理。一个典型的坑是:HTTP 服务端在请求处理异常时没关连接,客户端超时断开后,服务端停留在 CLOSEWAIT。排查方法:netstat 统计 CLOSEWAIT 数量,配合 lsof 找到对应进程,检查代码里的 socket 关闭逻辑。写段代码# 查看系统 TCP 连接状态分布import subprocessresult = subprocess.run(['netstat', '-tan'], capture_output=True, text=True)states = {}for line in result.stdout.splitlines()[2:]: parts = line.split() if len(parts) >= 6: state = parts[5] states[state] = states.get(state, 0) + 1for state, count in sorted(states.items(), key=lambda x: -x[1]): print(f"{state:20s} {count}")
计算机基础阅读 05月29日 00:10

TCP 拥塞控制的四个算法是什么?超时重传和快重传怎么区分?

TCP 拥塞控制有四个算法:慢启动、拥塞避免、快重传、快恢复。核心思想是"先试探再加码"——不清楚网络能承受多少时就少发,确认没问题再加速;一旦发现拥塞就立刻降速。控制对象是 cwnd(拥塞窗口),和 rwnd 取较小值作为实际发送窗口。追问慢启动为什么叫"慢"?明明指数增长很快"慢"是相对没有拥塞控制的情况——TCP 早期一上来就发满窗口,容易打爆网络。慢启动从 cwnd=1 MSS 开始,每收到一个 ACK 就 +1 MSS,一个 RTT 内翻倍(1→2→4→8→16)。看起来快,但初始只有 1 MSS,比直接发满窗口保守得多。cwnd 到达 ssthresh 后切换到拥塞避免的线性增长(每 RTT +1 MSS),不再翻倍。ssthresh 的初始值通常很大,第一次丢包后才设为 cwnd/2。3 个重复 ACK 为什么就重传?不等超时吗?等超时太慢了。RTO 通常是几百毫秒到几秒,而 3 个重复 ACK 说明后续包已经到了,只是中间那个丢了——网络还在工作,只是丢了一个包。快重传收到第 3 个重复 ACK 就立即重传,不用等 RTO,恢复速度差了一个数量级。为什么是 3 个不是 1 个?因为偶尔乱序也会产生重复 ACK,1 个就重传太激进,3 个基本确认是丢包。超时重传和快重传的处理有什么不同?超时重传意味着网络可能严重拥塞——连 ACK 都回不来了,所以处理更激进:ssthresh = cwnd/2,cwnd 直接降到 1 MSS,重新慢启动。快重传说明网络还通着(后续包到了),只是丢了一个包,所以处理温和:ssthresh = cwnd/2,cwnd 降到 ssthresh 而不是 1,进入快恢复。这就是为什么快重传后不回慢启动——丢一个包不代表网络崩溃。BBR 和传统拥塞控制有什么不同?传统算法(Reno/Cubic)基于丢包判断拥塞——丢包就降速,不丢就加速。问题是在高延迟高丢包的网络(无线、跨国)里,丢包不一定是拥塞,可能是链路本身的特性,传统算法会白白降速。BBR(Google 2016 年提出)不看病包,直接测量带宽和 RTT,把发送速率调整到带宽-延迟积(BDP),目标是把管道填满但不溢出。实测:YouTube 启用 BBR 后跨洲链路吞吐量提升 10-100 倍。写段代码# 模拟慢启动和拥塞避免的 cwnd 变化def simulate_cwnd(ssthresh=16, max_rtt=20): cwnd = 1 # 初始 1 MSS for rtt in range(1, max_rtt + 1): if cwnd < ssthresh: cwnd *= 2 # 慢启动:指数增长 phase = "慢启动" else: cwnd += 1 # 拥塞避免:线性增长 phase = "拥塞避免" print(f"RTT {rtt:2d}: cwnd={cwnd:3d} MSS [{phase}]")simulate_cwnd()
计算机基础阅读 05月29日 00:09

TCP 滑动窗口机制是怎么工作的?零窗口和糊涂窗口是什么?

TCP 流量控制靠滑动窗口:接收方在 ACK 里通告自己还能接收多少数据(rwnd),发送方据此控制发送量,不让接收方缓冲区溢出。发送窗口 = min(rwnd, cwnd),rwnd 是接收方给的流量控制信号,cwnd 是发送方自己根据拥塞状况算的。这里只说 rwnd 的部分。追问零窗口是怎么回事?怎么恢复?接收方缓冲区满了,在 ACK 里通告 rwnd=0,发送方就必须停。但接收方应用层读完数据释放缓冲区后,会发一个窗口更新报文告诉发送方可以继续了。问题在于:这个窗口更新报文丢了怎么办?发送方会一直等,死锁。解决方法是发送方启动持续计时器,定期发零窗口探测报文(只含 1 字节数据),迫使接收方回 ACK 并带上最新窗口值。这就是为什么零窗口不会永远卡住。糊涂窗口综合征是什么?接收方应用层每次只从缓冲区读 1 字节,就通告 rwnd=1;发送方就发 1 字节数据——一个 41 字节的包只装了 1 字节有效载荷,带宽利用率 2.4%。这就是糊涂窗口综合征(Silly Window Syndrome)。接收端的解决方法叫 Clark 方案:缓冲区空闲不到 MSS 或总缓冲区的 35% 时不通告新窗口;发送端配合 Nagle 算法,攒够数据再发。两端一起防才能根治。rwnd 和 cwnd 有什么区别?rwnd 是接收方告诉发送方"我还能收多少",防止接收方溢出;cwnd 是发送方自己算的"网络还能承受多少",防止网络拥塞。流量控制(rwnd)是端到端的保护,拥塞控制(cwnd)是全局性的保护。实际发送窗口取两者最小值——任何一个窗口满了都不能多发。面试中经常把这两个混在一起考,要分清楚哪个是对方给的,哪个是自己算的。滑动窗口的"滑动"体现在哪?窗口把发送缓冲区的数据分成四段:已确认 ← 窗口左边界 | 已发送未确认 | 可发送未发送 | 窗口右边界 → 不可发送。收到 ACK 后左边界右移(滑过去),接收方通告新窗口后右边界右移(窗口打开)——这就是"滑动"。如果右边界不变就是窗口关闭,左边界追上右边界就是零窗口。写段代码# 模拟滑动窗口发送def sliding_window_send(data, window_size): sent = 0 # 窗口左边界 acked = 0 # 已确认 while acked < len(data): # 发送窗口内的数据 while sent < min(acked + window_size, len(data)): print(f"发送 data[{sent}]") sent += 1 # 模拟收到 ACK print(f"收到 ACK={acked+1}") acked += 1 # 左边界滑动sliding_window_send(list(range(8)), window_size=3)
服务端阅读 05月29日 00:09

Python 多线程和多进程有什么区别?GIL 对多线程有什么影响?

核心区别:进程是资源分配单位,有独立内存空间;线程是调度单位,共享进程内存。在 Python 里这道题的特殊之处是 GIL——全局解释器锁让同一时刻只有一个线程执行 Python 字节码,所以 CPU 密集型任务用多线程不会加速,必须用多进程。I/O 密集型任务多线程够用,因为等 I/O 时 GIL 会释放。追问GIL 到底锁的是什么?为什么不能去掉?GIL 保护的是 CPython 的内存管理——引用计数。CPython 的对象引用计数不是线程安全的,如果多个线程同时修改引用计数,对象可能被提前释放或泄漏。加锁是最简单的方案,代价是多线程无法真正并行。Python 3.13 开始实验性支持 free-threaded 模式(PEP 703),试图移除 GIL,但目前生态兼容性还是问题。短期内 GIL 不会消失。多线程既然不能并行,还有用吗?有用。线程等待 I/O(网络请求、文件读写、数据库查询)时会释放 GIL,其他线程可以执行。所以 I/O 密集型场景(爬虫、API 调用、数据库操作)用多线程完全没问题。实测:100 个 HTTP 请求,单线程串行 10 秒,10 线程并发约 1.2 秒,接近 10 倍加速。但如果是纯计算(比如大矩阵运算),10 线程和 1 线程耗时几乎一样,甚至更慢——线程切换本身有开销。什么时候用多进程?有什么坑?CPU 密集型任务:数据处理、图像处理、机器学习训练。multiprocessing.Pool 是最常用的接口。最大的坑是进程间通信开销——进程不共享内存,数据要序列化传输。传一个大 numpy 数组给子进程,pickle 序列化的时间可能比计算本身还长。解决方案:用 multiprocessing.shared_memory(Python 3.8+)或 multiprocessing.Array 共享内存,避免序列化。另一个坑是子进程的异常不会自动传播到主进程,需要手动处理。协程和多线程有什么区别?协程是用户态的协作式调度,线程是内核态的抢占式调度。协程的切换由代码控制(await),线程的切换由操作系统控制。协程没有锁的问题——同一个时刻只有一个协程在执行,不存在竞态条件。代价是协程里不能有阻塞调用,一阻塞整个事件循环就卡住了。asyncio 适合高并发 I/O(上万连接的聊天服务器),threading 适合少量 I/O 并发或需要兼容阻塞库的场景。写段代码from multiprocessing import Poolimport timedef heavy_compute(n): return sum(i * i for i in range(n))if __name__ == '__main__': # 多进程:4核并行,接近4倍加速 with Pool(4) as p: results = p.map(heavy_compute, [10**7] * 4) print(results) # 对比:多线程对CPU密集型无效 # 4个线程和1个线程耗时几乎一样(GIL)