Skip to main content

跨域测试

info
你将学到
  • Cypress绕过同源策略的技术方案
  • 跨域内容的限制与解决方案

网络安全

浏览器遵循严格的同源策略。这意味着当<iframe>的源策略不匹配时,浏览器会限制它们之间的访问。

由于Cypress在浏览器内部工作,它必须始终能直接与远程应用通信。不幸的是,浏览器天然会阻止这种行为。

为解决这些限制,Cypress通过JavaScript代码、浏览器内部API和网络代理的组合策略来"遵守"同源策略规则。我们的目标是在无需修改应用代码的情况下完全自动化测试——我们基本实现了这一点。

Cypress的底层实现

  • 代理所有HTTP/HTTPS流量
  • 改变托管URL以匹配被测应用
  • 使用浏览器内部API处理网络层流量

当Cypress首次加载时,其内部应用会运行在随机端口,如http://localhost:64874/__/

在测试中执行第一个cy.visit()命令后,Cypress会将URL改为远程应用的源地址,从而解决同源策略的首要障碍。你的应用代码会像在Cypress外部一样执行,所有功能正常运作。

info
HTTPS如何支持?

Cypress必须分配和管理浏览器证书以实现实时流量修改。你会看到Chrome显示"SSL证书不匹配"的警告,这是预期行为。底层我们充当自己的CA机构,动态颁发证书来拦截原本无法访问的请求。我们仅对当前测试的源执行此操作,其他流量则直接放行。

注意,Cypress允许你为HTTPS站点可选地配置CA/客户端证书。详见配置客户端证书。如果远程服务器要求提供配置URL的客户端证书,Cypress会予以提供。

限制

尽管我们竭尽全力确保应用在Cypress中正常运行,但仍需注意以下限制:

不同源测试需要cy.origin()

Cypress会改变自身主机URL以匹配你的应用。除cy.origin外,Cypress要求单个测试中所有导航URL保持相同

如果尝试访问两个不同源,必须使用cy.origin包装第二个域的Cypress命令。否则,导航后命令会超时失败,因为这些命令实际仍在第一个域上执行。

没有cy.origin()时,你可以在不同测试中访问不同源,但不能在同一个测试中这样做。

不使用cy.origin会报错的测试案例

  1. 点击指向不同源的<a>链接后执行后续命令
  2. 提交导致重定向到不同源的<form>表单后执行命令
  3. 应用中执行如window.location.href = '...'跳转到不同源后执行命令

这些情况下,除非使用cy.origin命令,否则Cypress将失去自动化能力并报错。

继续阅读了解常见问题的解决方案

caution

重新启用 document.domain 注入

自 Cypress v14.0.0 起,默认不再向 text/html 页面注入 document.domain

这意味着现在当测试中需要在不同 之间导航时,必须使用 cy.origin()。此前,cy.origin() 仅在同一测试中导航至不同 超级域名 时才需要。

通过将 injectDocumentDomain 配置选项设为 true,Cypress 将尝试向 text/html 页面注入 document.domain

超级域名由主机名的最后两个元素组成,以 .(点号)分隔。例如 https://www.cypress.io 的超级域名是 cypress.io。启用此选项后,在 https://www.cypress.iohttps://docs.cypress.io 之间导航时不需要使用 cy.origin(),但在 https://www.cypress.iohttps://www.auth0.com 之间导航时仍需使用。

  • 这会导致某些网站出现问题,特别是那些使用 源键代理集群 的网站。
  • 此选项已被弃用,将在未来的 Cypress 版本中移除。

URL的组成部分

我们理解这些概念有些复杂,因此制作了示意图帮助理解!

URL分解图展示协议、主机、路径名、哈希等组成部分,以及主机名、端口、路径、查询参数的示例

由URL的协议主机名端口组成。以下URL与https://www.cypress.io同源:

  • https://www.cypress.io/cloud
  • https://www.cypress.io/app

但以下URL与https://www.cypress.io不同源:

  • http://www.cypress.io(协议不同)
  • https://docs.cypress.io(子域名不同)
  • https://www.auth0.com(主机名不同)
  • https://www.cypress.io:81(端口不同)
danger

不能在同一个测试中访问两个不同源后继续交互,除非使用cy.origin()

tip

可以不同测试中访问多个源,无需cy.origin()

实际应用如下:

// 这个测试能正常运行
it('导航', () => {
cy.visit('https://www.cypress.io')
cy.visit('https://www.cypress.io/app')
cy.get('选择器') // 完全正常
})
// 这会报错,因为https://docs.cypress.io与https://www.cypress.io不同源
it('导航', () => {
cy.visit('https://www.cypress.io')
cy.visit('https://docs.cypress.io')
cy.get('选择器')
})

修复上述跨域错误,使用cy.origin()指定后续命令的运行源:

it('导航', () => {
cy.visit('https://example.cypress.io')
cy.visit('https://docs.cypress.io')
cy.origin('https://docs.cypress.io', () => {
cy.get('选择器') // 完全正常
})
})
it('导航', () => {
cy.visit('https://www.cypress.io')
})

// 将不同源的访问拆分到另一个测试
it('导航到新源', () => {
cy.visit('https://cypress-dx.com')
cy.get('选择器') // 完全正常
})

跨域iframe

如果你的网站嵌入了跨域<iframe>,Cypress将无法自动化或与之通信。

跨域iframe的常见用途

  • 嵌入Vimeo或YouTube视频
  • 显示Stripe或Braintree的信用卡表单
  • 嵌入Auth0的登录表单
  • 展示Disqus评论

如果你需要此功能支持(查看我们的开放issue),或者可以禁用网络安全

不安全内容

由于Cypress的设计方式,当测试HTTPS站点时,任何导航回HTTP站点的尝试都会报错。这种行为有助于暴露应用中的严重安全问题。

访问不安全内容的例子

test.cy.js
cy.visit('https://example.cypress.io')

假设你的应用代码设置了cookies并存储了会话。现在想象应用中有一个不安全的链接(或JavaScript重定向)。

index.html
<html>
<a href="http://example.cypress.io/page2">页面2</a>
</html>

以下测试代码会立即失败:

test.cy.js
cy.visit('https://example.cypress.io')
cy.get('a').click() // 会失败

浏览器拒绝在安全页面显示不安全内容。因为Cypress初始URL是https://example.cypress.io,当浏览器跟随hrefhttp://example.cypress.io/page2时,会拒绝显示内容。

你可能会想:这似乎是Cypress的问题,因为我在Cypress之外使用时完全正常。

但实际上,Cypress暴露了你应用的安全漏洞,你希望它在Cypress中失败。

未设置secure标志为truecookies会以明文发送到不安全URL,使应用面临会话劫持风险。

即使你的服务器强制301重定向回HTTPS站点,最初的HTTP请求仍会暴露会话信息。

解决方案

更新HTML或JavaScript代码,避免导航到不安全HTTP页面,仅使用HTTPS。同时确保cookies设置了secure标志为true

如果无法控制代码,可以通过禁用网络安全绕过此限制。

同测试同端口

Cypress要求单个测试中所有导航URL保持相同端口(如指定)。这与浏览器标准同源策略行为一致。

常见解决方案

让我们探讨测试代码中可能遇到的跨域错误及其解决方案。

外部导航

最常见的跨域错误是点击导航到其他源的<a>标签。

index.html
<html>
<a href="https://example.cypress.io">Cypress</a>
</html>
test.cy.js
cy.visit('http://localhost:8080') // 你的web服务器和HTML所在位置
cy.get('a').click() // 浏览器导航到https://cypress.io
cy.get('选择器').should('exist') // Cypress报错
warning

我们不建议测试你不控制的站点

如果控制该源,建议使用cy.origin测试:

test.cy.js
cy.visit('http://localhost:8080')
cy.get('a').click()
cy.origin('https://example.cypress.io', () => {
// 在预期域上声明cy.origin
cy.get('选择器').should('exist') // 完全正常
})

如果不控制该源,建议测试href属性而非实际导航,这能提高测试确定性:

// 这个测试验证行为且运行更快
cy.visit('http://localhost:8080')
cy.get('a').should('have.attr', 'href', 'https://example.cypress.io') // 无需页面加载!

如果以上方法都不可行,可以使用cy.request()验证内容,因为它不受CORS或同源策略限制:

cy.visit('http://localhost:8080')
cy.get('a').then(($a) => {
const url = $a.prop('href')
cy.request(url).its('body').should('include', '</html>')
})

表单提交重定向

提交普通HTML表单时,浏览器会跟随HTTP(s)请求。

index.html
<html>
<form method="POST" action="/submit">
<input type="text" name="email" />
<input type="submit" value="提交" />
</form>
</html>
test.cy.js
cy.visit('http://localhost:8080')
cy.get('form').submit() // 提交表单!

如果后端服务器处理/submit路由时30x重定向到不同源,且提交表单后需要运行更多Cypress命令,则需要使用cy.origin

routes.js
// localhost:8080服务器上的node/express代码
app.post('/submit', (req, res) => {
res.redirect('https://example.cypress.io')
})

测试代码如下:

test.cy.js
cy.visit('http://localhost:8080')
cy.get('form').submit()
cy.origin('cypress.io', () => {
cy.url().should('contain', 'cypress.io')
})

常见用例包括单点登录(SSO)、OAuth、Open ID Connect (OIDC)或认证服务平台,如Auth0OktaAmazon Cognito等。

如果控制被测域,建议使用cy.origin测试:

test.cy.js
cy.visit('http://localhost:8080')
cy.get('#login').click()
cy.origin('cypress.io', () => {
cy.get('#username').type('用户1')
cy.get('#password').type('密码123')
cy.get('button').contains('登录').click()
})

cy.get('#user-name-welcome').should('equal', '欢迎,用户1!')

如果无法使用cy.origin,仍可采用编程式认证。这种情况下,你可以直接POST到SSO服务器:

cy.request('POST', 'https://sso.corp.com/auth', {
username: 'foo',
password: 'bar',
}).then((response) => {
const loc = response.headers['Location']
const token = parseOutMyToken(loc)
cy.visit('http://localhost:8080?token=' + token)
// 或者直接访问location头
cy.visit(loc)
})

JavaScript重定向

JavaScript重定向指类似以下的代码:

index.html
<html>
<button id="nav">导航到Cypress示例</button>
<script>
document.querySelector('#nav').addEventListener('click', () => {
window.location.href = 'https://example.cypress.io'
})
</script>
</html>

测试代码如下:

test.cy.js
cy.visit('http://localhost:8080')
cy.get('#nav').submit() // 触发JavaScript重定向!
cy.origin('https://example.cypress.io', () => {
cy.url().should('contain', 'cypress.io')
})

使用cy.origin时的跨域错误

有时,特别是测试非直接控制的网站时,跨域错误仍可能出现。我们不建议与不控制的站点交互。但如果必须,大多数问题可通过启用修改第三方干扰代码实验性标志或禁用网络安全解决。

禁用网络安全

如果无法通过上述方案解决问题,包括使用cy.origin修改第三方干扰代码,你可能需要禁用网络安全。

最后要考虑的是,我们偶尔会发现导致跨域错误的Cypress bug。如果你认为遇到bug,请提交issue

caution
仅限Chrome

禁用网络安全仅支持基于Chrome的浏览器。chromeWebSecurity设置对其他浏览器无效,此时会显示警告。

stdout中的chromeWebSecurity警告

如果依赖禁用网络安全功能,将无法在不支持此功能的浏览器上运行测试。

设置chromeWebSecurityfalse

在Chrome浏览器中设置chromeWebSecurityfalse允许你:

  • 显示不安全内容
  • 无需cy.origin即可无错导航到任何源
  • 访问应用中嵌入的跨域iframe

还在看?很好,让我们禁用网络安全!

Cypress配置中设置chromeWebSecurityfalse

const { defineConfig } = require('cypress')

module.exports = defineConfig({
chromeWebSecurity: false,
})

修改第三方干扰代码

Cypress具有修改干扰代码的概念,这些代码可能影响Cypress运行你的web应用。experimentalModifyObstructiveThirdPartyCode标志提供了与modifyObstructiveCode相同的优势,但还将其应用于应用中加载或导航到的第三方.js.html。此外,该标志还:

  • 调整Electron中的User Agent使其更像Chrome(可通过userAgent配置覆盖)
  • 从修改的脚本中移除子资源完整性(SRI),否则它们无法执行
  • 在来自被测应用的请求中,将Sec-Fetch-Dest元数据头从iframe更新为document

启用experimentalModifyObstructiveThirdPartyCode的方法:

const { defineConfig } = require('cypress')

module.exports = defineConfig({
experimentalModifyObstructiveThirdPartyCode: true,
})

其他解决方案

还有其他测试两个源交互的方法。浏览器有天然的源策略安全屏障,意味着localStoragecookiesservice workers等API不共享。Cypress提供了不受此限制的localStoragesessionStoragecookiesAPI。

最佳实践是不要访问或交互非你控制的网站。

如果你的组织使用单点登录(SSO)或OAuth,可以选择测试第三方服务而非你的源,这可以通过cy.origin()实现。

我们编写了多个相关指南: