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