问题

自动化测试

有的团队使用 Cypress 对站点进行自动化测试,但是很多场景是用户在登录态下进行的,需要首先解决登录问题。尽管预先注册账号,并将密码保存在一个安全的地方,在运行测试时动态获取,然后操作页面模拟用户登录。但是一般登录页面都会带上防机器人的脚本,直接阻止这样的自动化登录。比如有的网站使用了 WAF 服务商提供的 js 脚本,其工作原理见《<font style=color:rgb(18, 18, 18);>网络应用防火墙(WAF)防机器刷流量的工作原理,以登录举例 - Jeff Tian的文章 - 知乎 》。

尽管有些像黑客行为,但我仍然想提及接下来的解决方案。该方案要求正常人类用户先手动登录站点,获得 Cookie,然后在自动化脚本中直接使用该 Cookie 访问目标站点,从而直接进入登录态。

可能的解决方案

除了使用 Cookie 登录的方案,还有其他的方案。并且由于开发量较大,一般不建议使用该 Cookie 登录的方案。之所以列出来,一是因为有时候不得已时可以考虑;二是因为由于别的场景中已经实现了该方案,只不过正好也可以用来解决自动化测试的问题。所以,尽管开发量较大,但并不是为了自动化测试而专门开发的,于是边际成本变小了。

一、使用特殊的 HTTP 标头

在目标站点的代码里,做一个小改动。当检测到请求头里有一个特殊的字段,并且其值为允许的值时(比如 X-Internal=a-jwt-token),直接放行,不要应用机器人检测。

这个方案改动量很小,比如:

javascript // 如果头部 X-Internal 存在并且包含有效的访问令牌,则绕过机器人检测分析。 if (request.headers[X-Internal]) { if (await validateInternalAuthToken(request, request.headers[X-Internal][0].value, lambdaContext, requestId)) return request; else return buildBlockResponse(request, requestId); }

但是它要求允许的测试客户端小心地保存这个秘密值。不过,就算客户端保存得很好,但终究需要明文传输,总有可能会被暴露。

二、IP 白名单

这能轻而易举地解决问题,但是给服务器侧维护这个白名单带来了工作量,尤其当 IP 可能变化时。

三、给目标站点添加多个入口域名,在内部域名访问时,去除机器人检测

可以给站点一个公开域名,以及一个内部域名。内部域名只能使用 VPN 访问,相当于是内网环境,不用检测机器人。而对于公开域名,可以通过某些 DNS 服务来动态添加机器人检测脚本(比如 Cloudflare 这样的提供商)。

自动化测试使用内部域名来做。

Cookie 登录方案

有时候因为各种原因,以上可能的解决方案都不能用,那么可以考虑使用 Cookie 登录。这有点通过社会化工程,窃取终端用户的 Cookie,从而实现登录态。

前情提要

最早在《<font style=color:rgb(0, 0, 0);>如何优雅地将 markdown 文档同步到知乎专栏(叽歪同步工具介绍) - Jeff Tian的视频 - 知乎 》以及《<font style=color:rgb(0, 0, 0);>如何优雅地将 markdown 文档同步到知乎专栏(叽歪同步工具介绍之有头模式) - Jeff Tian的视频 - 知乎 》中提到的“叽歪同步工具”自动化发布知乎专栏,就是使用了这个方案。由于该视频的点赞量还没有到 1000,所以并未开源整个解决方案。不过,今天会公开一下架构图和核心代码,因为它的用处不局限于自动化发布知乎专栏。

如何获取用户 Cookie?

如果是浏览器登录,一般可以通过 F12 打开开发者工具栏看到 Cookie: 1687944907723 74066688 e918 462a b534 aa1dd7017dd9

对于自动化测试来说,你自己正常登录后,将 Cookie 手动保存下来即可。对于其他场景,你可能需要自已做一个站点,来收集用户的 Cookie。《<font style=color:rgb(0, 0, 0);>扫二维码登录一定安全吗? - Jeff Tian的视频 - 知乎 》中有提到我开发的一个收集知乎 Cookie 的站点。

架构图

大致如下:

1687945069574 a4fff620 cbf0 4092 87c3 72189dc3e7a0

核心代码

Cypress 启动时,禁用浏览器安全:

javascript const {defineConfig} = require(cypress);

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

运行测试用例前,获取 Cookie:

javascript describe(feature, ()=> { let cookie

before(() => {
cy.request( POST, THE_PROXY_API_FOR_GET_COOKIE, { query: a graphql request body, variables: {key: user-cookie-60808105033728} } ).then((response) => { cookie = JSON.parse(response.body.data.cookie); }) })

it(should login automatically and do stuff, () => { login(cookie); ... }) })

使用 Cookie 登录:

javascript function login(cookie) { // 一开始未登录 cy.visit(https://www.the.site) cy.clearCookies(); Cypress.Cookies.debug(true) // Cypress 替换 Cookie 时,会打印日志显示出来

JSON.parse(cookie).data.map(item => {
    const cookie = Cookie.parse(item);

    cy.setCookie(cookie.key, cookie.value, {
        expiry: (!!cookie.expires && cookie.expires !== Infinity) ? new Date(cookie.expires).getTime() / 1000 : undefined,
        path: cookie.path,
        domain: cookie.domain ? cookie.domain : undefined,
        secure: cookie.secure,
    })
});

// 现在是登录态了
cy.visit(https://www.the.site)

}