面试题手册

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

服务端阅读 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);