参考同级项目 novavis-authority 实现,在 HTTPS 基础上增加应用层加密。 后端: - CryptoService: AES-256-GCM + PBKDF2 密钥派生(manage-common) - CryptoFilter: Gateway GlobalFilter,检测 X-Encrypted 头后加解密请求/响应体 - 配置:app.encryption.secret 通过环境变量注入 前端: - crypto.ts: Web Crypto API 实现 AES-256-GCM + PBKDF2 - request.ts: 拦截器自动加密请求体、解密响应体 - 环境变量 VITE_ENCRYPTION_SECRET 测试覆盖: - 后端 CryptoServiceTest 16 个用例 + CryptoFilterTest 7 个用例 - 前端 crypto.test.ts 8 个用例 + Playwright E2E smoke 测试 - 全量 510 个前端测试 + 后端全量测试全部通过,无回归
133 lines
5.1 KiB
TypeScript
133 lines
5.1 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
test.describe('前后端加密通信验证', () => {
|
|
test.use({ storageState: { cookies: [], origins: [] } });
|
|
|
|
test('登录请求应被加密(请求体非明文JSON,响应体非明文JSON)', async ({ page }) => {
|
|
// 用于捕获登录 API 请求和响应的变量
|
|
let capturedRequestUrl: string | null = null;
|
|
let capturedRequestBody: string | null = null;
|
|
let capturedRequestHeaders: Record<string, string> = {};
|
|
let capturedResponseStatus: number | null = null;
|
|
let capturedResponseBody: string | null = null;
|
|
let capturedResponseHeaders: Record<string, string> = {};
|
|
|
|
// 监听 API 请求——在请求发送前捕获
|
|
page.on('request', request => {
|
|
if (request.url().includes('/api/auth/login') && request.method() === 'POST') {
|
|
capturedRequestUrl = request.url();
|
|
capturedRequestHeaders = request.headers() as Record<string, string>;
|
|
// 对于 POST 请求,Playwright 无法直接获取 body,通过 route 拦截
|
|
}
|
|
});
|
|
|
|
// 使用 route 拦截请求体
|
|
await page.route('**/api/auth/login', async (route) => {
|
|
const postData = route.request().postData();
|
|
if (postData) {
|
|
capturedRequestBody = postData;
|
|
}
|
|
|
|
// 继续请求,但拦截响应
|
|
const response = await route.fetch();
|
|
capturedResponseStatus = response.status();
|
|
capturedResponseHeaders = response.headers() as Record<string, string>;
|
|
capturedResponseBody = await response.text();
|
|
|
|
// 继续原始响应
|
|
await route.fulfill({ response });
|
|
});
|
|
|
|
// 导航到登录页
|
|
await page.goto('/login');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// 填入登录信息
|
|
await page.fill('input[type="text"], input[placeholder*="用户名"]', 'admin');
|
|
await page.fill('input[type="password"], input[placeholder*="密码"]', 'Test@123');
|
|
|
|
// 点击登录
|
|
await page.click('button:has-text("登录")');
|
|
|
|
// 等待登录完成
|
|
await page.waitForURL(/.*dashboard/, { timeout: 30000 });
|
|
|
|
// ========== 验证请求体加密 ==========
|
|
await test.step('验证请求体已加密(非明文JSON)', async () => {
|
|
expect(capturedRequestBody).toBeTruthy();
|
|
|
|
// 请求体不应是明文 JSON(不应包含 username 字段)
|
|
expect(capturedRequestBody).not.toContain('"username"');
|
|
expect(capturedRequestBody).not.toContain('"admin"');
|
|
expect(capturedRequestBody).not.toContain('"password"');
|
|
|
|
// 请求体应为 Base64 编码(AES-GCM 密文)
|
|
expect(capturedRequestBody).toMatch(/^[A-Za-z0-9+/=]+$/);
|
|
});
|
|
|
|
await test.step('验证 X-Encrypted 请求头存在', async () => {
|
|
expect(capturedRequestHeaders['x-encrypted']?.toLowerCase()).toBe('true');
|
|
});
|
|
|
|
// ========== 验证响应体加密 ==========
|
|
await test.step('验证响应体已加密(非明文JSON)', async () => {
|
|
expect(capturedResponseBody).toBeTruthy();
|
|
expect(capturedResponseStatus).toBe(200);
|
|
|
|
// 响应体不应是明文 JSON(不应包含 token 字段)
|
|
expect(capturedResponseBody).not.toContain('"token"');
|
|
|
|
// 响应体应为 Base64 编码
|
|
expect(capturedResponseBody).toMatch(/^[A-Za-z0-9+/=]+$/);
|
|
});
|
|
|
|
await test.step('验证 X-Encrypted 响应头存在', async () => {
|
|
expect(capturedResponseHeaders['x-encrypted']?.toLowerCase()).toBe('true');
|
|
});
|
|
|
|
// ========== 验证业务正常 ==========
|
|
await test.step('验证登录成功——页面跳转到 dashboard', async () => {
|
|
await expect(page).toHaveURL(/.*dashboard/);
|
|
});
|
|
});
|
|
|
|
test('GET 请求的响应体也应被加密', async ({ page }) => {
|
|
// 先登录
|
|
await page.goto('/login');
|
|
await page.waitForLoadState('networkidle');
|
|
await page.fill('input[type="text"], input[placeholder*="用户名"]', 'admin');
|
|
await page.fill('input[type="password"], input[placeholder*="密码"]', 'Test@123');
|
|
await page.click('button:has-text("登录")');
|
|
await page.waitForURL(/.*dashboard/, { timeout: 30000 });
|
|
|
|
// 捕获 GET 请求
|
|
let capturedGetBody: string | null = null;
|
|
let capturedGetEncrypted: string | null = null;
|
|
|
|
await page.route('**/api/users**', async (route) => {
|
|
// 仅拦截 GET 请求
|
|
if (route.request().method() === 'GET') {
|
|
const response = await route.fetch();
|
|
capturedGetEncrypted = response.headers()['x-encrypted'] ?? null;
|
|
capturedGetBody = await response.text();
|
|
await route.fulfill({ response });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
// 导航到用户管理页面触发 GET 请求
|
|
await page.goto('/users');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await test.step('验证 GET 响应已加密', async () => {
|
|
// 注意:如果用户无权限访问 /users,可能没有加密响应
|
|
// 这里只验证如果响应存在,且标记了加密,则响应体应为 Base64
|
|
if (capturedGetEncrypted) {
|
|
expect(capturedGetEncrypted).toBe('true');
|
|
expect(capturedGetBody).toMatch(/^[A-Za-z0-9+/=]+$/);
|
|
expect(capturedGetBody).not.toContain('"id"');
|
|
}
|
|
});
|
|
});
|
|
}); |