Files
gym-manage/gym-manage-web/e2e/smoke/encryption-smoke.spec.ts
T
zhangxiang eb33755f23 feat(encryption): 实现前后端 AES-256-GCM 加密通信
参考同级项目 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 个前端测试 + 后端全量测试全部通过,无回归
2026-08-02 08:51:21 +08:00

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"');
}
});
});
});