5月27日 23:03

Cypress 中怎么处理认证和授权?从 cy.session 到多角色测试的实战方案

在端到端测试中,认证和授权是最容易出问题的环节。登录流程写不好,测试就跑不通;权限验证不充分,线上就出漏洞。Cypress 提供了 cy.session()cy.request()cy.intercept() 等一整套工具来处理这些场景,但很多项目还在用最原始的方式——每个测试用例都跑一遍登录 UI,既慢又不稳定。

这篇文章从实际项目出发,讲清楚 Cypress 中处理认证和授权的几种典型模式,包括会话管理、程序化登录、多角色切换、JWT/OAuth 集成,以及常见坑点。

用 cy.session() 管理登录会话

cy.session() 是 Cypress 处理认证的核心命令。它的作用很简单:第一次执行真正的登录流程,然后把 cookies、localStorage、sessionStorage 全部缓存起来,后续测试直接恢复,不再重复登录。

基本用法

javascript
// cypress/support/commands.js Cypress.Commands.add('login', (username, password) => { cy.session([username, password], () => { cy.visit('/login') cy.get('[data-testid=username]').type(username) cy.get('[data-testid=password]').type(password) cy.get('[data-testid=submit]').click() cy.url().should('contain', '/dashboard') }) })

在测试中使用:

javascript
describe('受保护页面', () => { beforeEach(() => { cy.login('testuser', 'password123') }) it('应该能看到仪表盘', () => { cy.visit('/dashboard') cy.get('[data-testid=welcome]').should('contain', 'Welcome') }) })

注意一点:cy.session() 执行后,页面会被重置为 about:blank,所以每个测试用例里必须显式调用 cy.visit() 去访问目标页面。

用 validate 检查会话是否还有效

如果 token 有过期时间,可以给 cy.session() 加一个 validate 回调。Cypress 每次恢复会话前都会先跑这个验证,失败了就重新登录:

javascript
Cypress.Commands.add('loginWithValidation', (username, password) => { cy.session([username, password], () => { cy.visit('/login') cy.get('[data-testid=username]').type(username) cy.get('[data-testid=password]').type(password) cy.get('[data-testid=submit]').click() cy.url().should('contain', '/dashboard') }, { validate() { cy.request('/api/auth/me').its('status').should('eq', 200) } }) })

这样即使 token 过期了,测试也不会因为 401 而挂掉。

程序化登录:跳过 UI 直接到 API 拿 token

UI 登录慢,而且容易因为页面改动而断裂。对于测试来说,更稳的做法是直接调 API 拿 token,然后注入到浏览器中。

基于 JWT 的程序化登录

javascript
Cypress.Commands.add('loginByAPI', () => { cy.session('api-user', () => { cy.request('POST', '/api/auth/login', { username: Cypress.env('TEST_USERNAME'), password: Cypress.env('TEST_PASSWORD') }).then(({ body }) => { // 把 token 存到 localStorage window.localStorage.setItem('auth_token', body.token) }) }) })

环境变量放在 cypress.env.json 里:

json
{ "TEST_USERNAME": "testuser@example.com", "TEST_PASSWORD": "securepassword" }

这种方式比 UI 登录快好几倍,而且不受页面样式变化影响。

处理 OAuth / 第三方登录

Cypress 官方不建议在测试中去操作第三方登录页面(比如 Google、GitHub 的 OAuth 页面),因为那些页面不受你控制,随时可能改版导致测试失败。正确的做法是程序化获取 token:

javascript
// 以 Auth0 为例 Cypress.Commands.add('loginByAuth0', () => { cy.session('auth0-user', () => { cy.request({ method: 'POST', url: `https://${Cypress.env('AUTH0_DOMAIN')}/oauth/token`, body: { grant_type: 'password', client_id: Cypress.env('AUTH0_CLIENT_ID'), client_secret: Cypress.env('AUTH0_CLIENT_SECRET'), username: Cypress.env('AUTH0_USERNAME'), password: Cypress.env('AUTH0_PASSWORD'), audience: Cypress.env('AUTH0_AUDIENCE'), scope: 'openid profile email' } }).then(({ body }) => { // Auth0 返回 access_token 和 id_token window.localStorage.setItem('auth0_token', body.access_token) }) }) })

需要在 Auth0 后台开启 Password Grant 类型,并创建专门的测试用户。

授权测试:验证不同角色的访问权限

认证解决的是"你是谁"的问题,授权解决的是"你能干什么"的问题。在测试中,最关键的是确保不同角色看到的内容和能执行的操作是正确的。

多角色会话管理

cy.session() 的不同 ID 来管理多个角色:

javascript
// 管理员登录 Cypress.Commands.add('loginAsAdmin', () => { cy.session('admin-user', () => { cy.request('POST', '/api/auth/login', { username: Cypress.env('ADMIN_USERNAME'), password: Cypress.env('ADMIN_PASSWORD') }).then(({ body }) => { window.localStorage.setItem('auth_token', body.token) }) }) }) // 普通用户登录 Cypress.Commands.add('loginAsUser', () => { cy.session('regular-user', () => { cy.request('POST', '/api/auth/login', { username: Cypress.env('USER_USERNAME'), password: Cypress.env('USER_PASSWORD') }).then(({ body }) => { window.localStorage.setItem('auth_token', body.token) }) }) })

用 cy.intercept() 验证 API 权限

javascript
describe('管理员权限', () => { beforeEach(() => { cy.loginAsAdmin() cy.intercept('GET', '/api/admin/users').as('getUsers') }) it('管理员应该能访问用户列表', () => { cy.visit('/admin/users') cy.wait('@getUsers').its('response.statusCode').should('eq', 200) cy.get('[data-testid=user-list]').should('be.visible') }) }) describe('普通用户权限', () => { beforeEach(() => { cy.loginAsUser() }) it('普通用户不应该看到管理后台', () => { cy.request({ url: '/api/admin/users', failOnStatusCode: false }).its('status').should('eq', 403) }) })

这里有个细节:cy.request() 默认在收到 4xx 状态码时会抛错,加上 failOnStatusCode: false 才能正常断言 403。

Cookies 和 Storage 的操作与清理

认证状态通常存储在 cookies 或 localStorage 中,Cypress 提供了专门的 API 来操作它们。

读取和验证

javascript
// 验证认证 cookie 存在 cy.getCookie('session_id').should('exist') // 检查 cookie 值和安全属性 cy.getCookie('auth_token').then((cookie) => { expect(cookie.value).to.include('Bearer') expect(cookie.httpOnly).to.be.true // 确保 HttpOnly 标记 expect(cookie.secure).to.be.true // 确保 Secure 标记 }) // 操作 localStorage cy.window().then((win) => { const token = win.localStorage.getItem('auth_token') expect(token).to.not.be.null })

测试隔离:每个用例前清理状态

javascript
beforeEach(() => { cy.clearCookies() cy.clearLocalStorage() })

这一步很重要。如果不清理,上一个测试的登录状态可能"泄漏"到下一个测试,导致本应失败的测试意外通过。

常见坑和解决方案

坑 1:cy.session() 后忘记 cy.visit()

cy.session() 执行后会重置页面到 about:blank。如果你直接在后面断言页面元素,一定会失败。必须在 cy.session() 之后、断言之前调用 cy.visit()

javascript
// 错误写法 beforeEach(() => { cy.login('user', 'pass') // 页面是 about:blank,下面的断言会失败 cy.get('h1').should('contain', 'Dashboard') }) // 正确写法 beforeEach(() => { cy.login('user', 'pass') cy.visit('/dashboard') cy.get('h1').should('contain', 'Dashboard') })

坑 2:硬编码凭据

不要把用户名密码直接写在测试代码里。用 cypress.env.json(已被 gitignore)或 CI 环境变量来管理:

javascript
// 错误 cy.get('input[name=username]').type('admin') cy.get('input[name=password]').type('123456') // 正确 cy.get('input[name=username]').type(Cypress.env('ADMIN_USERNAME')) cy.get('input[name=password]').type(Cypress.env('ADMIN_PASSWORD'))

坑 3:跨域认证测试

当登录页面和应用页面不在同一个域下时(比如 Auth0 登录在 auth0.com,应用在 example.com),需要用 cy.origin() 来处理跨域操作:

javascript
Cypress.Commands.add('loginWithSSO', () => { cy.session('sso-user', () => { cy.visit('/login') cy.get('[data-testid=sso-button]').click() cy.origin('https://auth.example.com', () => { cy.get('input[name=email]').type(Cypress.env('SSO_EMAIL')) cy.get('input[name=password]').type(Cypress.env('SSO_PASSWORD')) cy.get('button[type=submit]').click() }) cy.url().should('contain', '/dashboard') }) })

坑 4:测试间状态泄漏

如果某个测试修改了用户角色或权限,后续测试可能因为这个修改而行为异常。解决办法是确保每个测试有独立的初始状态:

javascript
// 用 cy.session() 自动隔离 // 用 cy.database() 或 cy.task() 重置测试数据 before(() => { cy.task('db:seed') })

完整示例:一个认证测试套件

把上面的内容整合起来,一个实际项目中可用的认证测试套件大概长这样:

javascript
// cypress/support/commands.js Cypress.Commands.add('login', (role = 'user') => { const accounts = { admin: { username: Cypress.env('ADMIN_USER'), password: Cypress.env('ADMIN_PASS') }, user: { username: Cypress.env('TEST_USER'), password: Cypress.env('TEST_PASS') }, guest: { username: Cypress.env('GUEST_USER'), password: Cypress.env('GUEST_PASS') } } const account = accounts[role] cy.session(`user-${role}`, () => { cy.request('POST', '/api/auth/login', account).then(({ body }) => { window.localStorage.setItem('auth_token', body.token) }) }, { validate() { cy.request({ url: '/api/auth/me', failOnStatusCode: false }) .its('status').should('eq', 200) } }) })
javascript
// cypress/e2e/auth.cy.js describe('认证流程', () => { it('未登录用户应该被重定向到登录页', () => { cy.visit('/dashboard') cy.url().should('include', '/login') }) it('登录成功后应该跳转到仪表盘', () => { cy.visit('/login') cy.get('[data-testid=username]').type(Cypress.env('TEST_USER')) cy.get('[data-testid=password]').type(Cypress.env('TEST_PASS')) cy.get('[data-testid=submit]').click() cy.url().should('include', '/dashboard') }) it('错误的密码应该显示错误提示', () => { cy.visit('/login') cy.get('[data-testid=username]').type(Cypress.env('TEST_USER')) cy.get('[data-testid=password]').type('wrongpassword') cy.get('[data-testid=submit]').click() cy.get('[data-testid=error-message]').should('be.visible') }) }) describe('权限控制', () => { it('管理员可以访问设置页', () => { cy.login('admin') cy.visit('/settings') cy.get('[data-testid=settings-panel]').should('be.visible') }) it('普通用户不能访问设置页', () => { cy.login('user') cy.request({ url: '/api/admin/settings', failOnStatusCode: false }) .its('status').should('eq', 403) }) })

这套方案的核心思路是:登录流程只测一次 UI,其余全部走 API;权限测试通过角色切换覆盖不同场景;cy.session() + validate 保证会话有效且不重复登录。

关于认证测试的更多细节,可以参考 Cypress Authentication 官方指南cy.session() API 文档

标签:Cypress