摘要: 当你的应用采用前后端分离架构时,CORS配置的正确性至关重要。但如何自动化测试它?本文将打破"用Selenium绕过CORS"的迷思,提供两套实用的自动化测试方案,让你能够系统化地验证CORS配置,确保跨域访问安全可控。
关键词:CORS自动化测试、Playwright、API测试、预检请求、跨域验证
一、破除迷思:自动化测试CORS的目标是什么?
在开始之前,我们必须明确一个关键点:我们的目标不是绕过浏览器的同源策略,而是验证CORS配置是否正确工作。
许多开发者误以为可以用自动化工具"解决"CORS问题,这是错误的认知。浏览器的同源策略是安全基石,不可也不应绕过。我们的自动化测试应该验证:
- 限制是否生效:来自未授权域的请求是否被正确拦截?
- 授权是否正确:来自授权域的请求是否能够正常通行?
- 配置是否完整:预检请求、各种HTTP方法、自定义头是否都得到正确处理?
二、方案一:Playwright/Selenium - 浏览器环境验证
这种方法模拟真实用户行为,验证前端在跨域场景下的表现。
测试场景设计
// test-cors-ui.spec.js
const { test, expect } = require('@playwright/test');
test.describe('CORS UI 行为验证', () => {
test('未配置CORS时应触发浏览器错误', async ({ page }) => {
// 监听控制台错误
const consoleErrors = [];
page.on('console', msg => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text());
}
});
// 访问测试页面,该页面会向未配置CORS的后端发起请求
await page.goto('http://localhost:3000/test-cors');
// 触发跨域请求
await page.click('#trigger-request');
// 等待请求完成
await page.waitForTimeout(1000);
// 验证控制台出现了CORS错误
const corsError = consoleErrors.find(error =>
error.includes('CORS') || error.includes('Access-Control-Allow-Origin')
);
expect(corsError).toBeTruthy();
});
test('正确配置CORS后请求应成功', async ({ page }) => {
// 访问配置了正确CORS的环境
await page.goto('http://localhost:3000/test-cors');
// 触发请求并等待响应
const responsePromise = page.waitForResponse('**/api/data');
await page.click('#trigger-request');
const response = await responsePromise;
// 验证请求成功
expect(response.status()).toBe(200);
// 验证页面显示成功状态
const statusElement = page.locator('#request-status');
await expect(statusElement).toHaveText('success');
});
});
优缺点分析
优点:
- 最接近真实用户场景
- 能发现前端代码中的跨域处理问题
缺点:
- 测试速度较慢
- 错误信息依赖于浏览器控制台,不够稳定
- 无法精细测试后端的CORS配置细节
三、方案二:API测试为主,UI测试为辅(推荐)
这是更直接、更可靠的方案,直接测试后端的CORS配置。
2.1 使用Playwright进行API测试
// test-cors-api.spec.js
const { test, expect } = require('@playwright/test');
const ALLOWED_ORIGIN = 'https://your-allowed-domain.com';
const DISALLOWED_ORIGIN = 'https://evil-domain.com';
const API_ENDPOINT = '/api/data';
test.describe('CORS API 配置测试', () => {
test('预检请求(OPTIONS)应返回正确的CORS头', async ({ request }) => {
const response = await request.options(API_ENDPOINT, {
headers: {
'Origin': ALLOWED_ORIGIN,
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'Content-Type, Authorization',
}
});
// 验证预检请求成功
expect(response.status()).toBe(204);
// 验证CORS头部正确设置
const headers = response.headers();
expect(headers['access-control-allow-origin']).toBe(ALLOWED_ORIGIN);
expect(headers['access-control-allow-methods']).toContain('POST');
expect(headers['access-control-allow-headers']).toContain('Content-Type');
expect(headers['access-control-allow-headers']).toContain('Authorization');
// 验证是否允许凭证(如果需要)
expect(headers['access-control-allow-credentials']).toBe('true');
});
test('实际请求应包含CORS头', async ({ request }) => {
const response = await request.post(API_ENDPOINT, {
headers: {
'Origin': ALLOWED_ORIGIN,
'Content-Type': 'application/json',
},
data: { test: 'data' }
});
expect(response.status()).toBe(200);
expect(response.headers()['access-control-allow-origin']).toBe(ALLOWED_ORIGIN);
});
test('未授权源的请求应被拒绝', async ({ request }) => {
const response = await request.post(API_ENDPOINT, {
headers: {
'Origin': DISALLOWED_ORIGIN,
'Content-Type': 'application/json',
},
data: { test: 'data' }
});
// 注意:API测试中,请求本身会成功(因为不是浏览器环境)
// 但我们能验证服务器没有返回该源的Allow-Origin头
const allowOrigin = response.headers()['access-control-allow-origin'];
expect(allowOrigin).not.toBe(DISALLOWED_ORIGIN);
// 或者验证返回了具体的错误状态码
expect(response.status()).toBe(403); // 如果后端配置了严格的检查
});
test('测试不同的HTTP方法', async ({ request }) => {
const methods = ['GET', 'POST', 'PUT', 'DELETE'];
for (const method of methods) {
const response = await request.fetch(API_ENDPOINT, {
method: method,
headers: { 'Origin': ALLOWED_ORIGIN }
});
expect(response.status()).toBe(method === 'GET' ? 200 : 201);
expect(response.headers()['access-control-allow-origin']).toBe(ALLOWED_ORIGIN);
}
});
});
2.2 使用Jest + Axios进行单元测试
// cors-service.test.js
const axios = require('axios');
describe('CORS配置服务测试', () => {
const API_BASE_URL = 'http://localhost:8080';
test('验证CORS头配置', async () => {
const response = await axios.options(`${API_BASE_URL}/api/data`, {
headers: {
'Origin': 'https://your-allowed-domain.com',
'Access-Control-Request-Method': 'POST'
}
});
expect(response.status).toBe(204);
expect(response.headers['access-control-allow-origin']).toBe('https://your-allowed-domain.com');
expect(response.headers['access-control-allow-methods']).toMatch(/POST/);
});
test('验证复杂请求的CORS处理', async () => {
try {
const response = await axios.post(`${API_BASE_URL}/api/data`,
{ data: 'test' },
{
headers: {
'Origin': 'https://your-allowed-domain.com',
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
}
}
);
expect(response.headers['access-control-allow-origin']).toBe('https://your-allowed-domain.com');
expect(response.headers['access-control-expose-headers']).toContain('X-Custom-Header');
} catch (error) {
// 处理预期的CORS错误
expect(error.response.status).toBe(403);
}
});
});
四、完整的测试策略建议
分层测试金字塔
↗ UI行为验证 (Playwright/Selenium)
↗ API集成测试 (Playwright API Testing)
↗ 单元测试 (Jest + 模拟请求)
持续集成中的CORS测试
# .github/workflows/test-cors.yml
name: CORS Configuration Tests
on: [push, pull_request]
jobs:
cors-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm install
- name: Start test server
run: npm run test:server &
- name: Wait for server
run: npx wait-on http://localhost:8080/health
- name: Run CORS API tests
run: npm run test:cors-api
- name: Run CORS UI tests
run: npm run test:cors-ui
五、最佳实践与常见陷阱
该测试的:
- ✅ 预检请求(OPTIONS)的响应头
- ✅ 各种HTTP方法的CORS支持
- ✅ 授权域与未授权域的差异化处理
- ✅ 自定义头的支持情况
- ✅ 凭证模式(withCredentials)的支持
不该测试的:
- ❌ 试图"绕过"浏览器的同源策略
- ❌ 浏览器具体的错误消息文本(可能因浏览器版本而异)
- ❌ 网络层的超时问题(这不是CORS问题)
常见陷阱:
- 通配符*与凭证模式的冲突:当使用Access-Control-Allow-Credentials: true 时,不能使用Access-Control-Allow-Origin: *
- 缓存问题:预检请求结果可能被缓存,影响测试结果
- 环境差异:开发、测试、生产环境的CORS配置可能不同
六、总结
自动化测试CORS配置不仅是可行的,而且是必要的。通过本文介绍的两种方案:
- API测试为主:直接、快速、可靠地验证后端CORS配置
- UI测试为辅:验证前端在真实浏览器环境下的跨域行为
建议采用"API测试为主,UI测试为辅"的策略,在持续集成流水线中自动运行这些测试,确保CORS配置在任何环境变更时都能保持正确。
2万+

被折叠的 条评论
为什么被折叠?



