Files
chunyu_project/templates/test_slider_captcha.html
root 3618323192 fix(security): P1/P2 审计修复 + JWT HttpOnly Cookie 双模认证 + 限流
- P0/P1 审计修复: 滑块验证码不再下发 x_position/成败即销毁 key、
  user-login 补失败计数+滑块门控、限流标识改 X-Real-IP、
  百度翻译 appkey 环境化、ChangeEmail/ChangePhone 补调 avalidate、
  logs/tasks.py Count(filter=Q) 修复、chat 收藏 SSRF 内网黑名单
- P1 #6/7: token_blacklist + ROTATE_REFRESH_TOKENS 开启,
  /user/token/refresh/ 挂载
- #2 JWT HttpOnly Cookie 双模认证: user/cookie_auth.py 种/清 Cookie,
  user/authentication.py CookieOrHeaderJWTAuthentication(Bearer 优先/_COOKIE 兜底),
  user/views/token.py CookieTokenRefreshView + UserLogoutAPIView(/user/logout/),
  create_standardized_response 自动对含 token 的响应种 Cookie,
  异步视图内 RefreshToken.for_user 全部 sync_to_async 包裹(修 SynchronousOnlyOperation 500),
  WS ChatConsumer 优先读 Cookie token
- P2 #11 限流: utils/rate_limit.py 固定窗口频控,
  shorturl 生成 匿名10次/分+登录60次/分, 邮箱验证码 同邮箱60s1次+同IP10次/10min,
  登录/注册验证码 错5次作废+成功即销毁防重放, 换绑邮箱/手机 同步落地,
  urls.py 补挂 shorturl 路由(此前 404)
2026-09-08 11:28:00 +08:00

533 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>滑块验证码测试</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 32px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.test-section {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
.test-section h2 {
color: #333;
margin-bottom: 16px;
font-size: 20px;
border-bottom: 2px solid #667eea;
padding-bottom: 8px;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: transform 0.2s, box-shadow 0.2s;
margin-right: 10px;
margin-bottom: 10px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn:active {
transform: translateY(0);
}
.btn-success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.btn-danger {
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
}
.btn-warning {
background: linear-gradient(135deg, #f2994a 0%, #f2c94c 100%);
}
.log-container {
background: #1e1e1e;
border-radius: 8px;
padding: 16px;
margin-top: 16px;
max-height: 400px;
overflow-y: auto;
}
.log-entry {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
color: #d4d4d4;
padding: 4px 0;
border-bottom: 1px solid #333;
}
.log-entry:last-child {
border-bottom: none;
}
.log-entry.success {
color: #4ec9b0;
}
.log-entry.error {
color: #f44747;
}
.log-entry.info {
color: #569cd6;
}
.log-entry .timestamp {
color: #858585;
margin-right: 10px;
}
.captcha-container {
margin: 20px 0;
}
#captcha-display {
background: #f5f5f5;
border-radius: 8px;
padding: 20px;
text-align: center;
}
#captcha-display img {
max-width: 100%;
border-radius: 4px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
color: #333;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.result-box {
background: #f8f9fa;
border-left: 4px solid #667eea;
padding: 12px 16px;
border-radius: 4px;
margin-top: 16px;
}
.result-box pre {
white-space: pre-wrap;
word-wrap: break-word;
font-family: 'Consolas', monospace;
font-size: 13px;
color: #333;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-indicator.success {
background: #4ec9b0;
}
.status-indicator.error {
background: #f44747;
}
.status-indicator.pending {
background: #f2c94c;
}
</style>
</head>
<body>
<div class="container">
<h1>🎯 滑块验证码浏览器测试</h1>
<!-- API配置 -->
<div class="test-section">
<h2><span class="status-indicator pending" id="server-status"></span>服务器状态</h2>
<div class="form-group">
<label>API基础URL</label>
<input type="text" id="api-base-url" value="http://localhost:8000" placeholder="http://localhost:8000">
</div>
<button class="btn" onclick="checkServerStatus()">检查服务器连接</button>
</div>
<!-- 生成验证码测试 -->
<div class="test-section">
<h2>1. 生成滑块验证码</h2>
<button class="btn btn-success" onclick="generateCaptcha()">生成新验证码</button>
<button class="btn btn-warning" onclick="refreshCaptcha()">刷新验证码</button>
<div class="captcha-container" id="captcha-display" style="display:none;">
<h3>背景图 + 滑块</h3>
<p id="captcha-info" style="color: #666; margin-bottom: 10px;"></p>
<div style="position: relative; display: inline-block;">
<img id="bg-image" alt="背景图">
<img id="slider-image" alt="滑块" style="position: absolute; top: 0; left: 0; pointer-events: none;">
</div>
<p id="captcha-key-display" style="margin-top: 10px; font-family: monospace;"></p>
</div>
<div class="result-box" id="generate-result" style="display:none;">
<pre id="generate-result-content"></pre>
</div>
</div>
<!-- 滑块拖拽测试 -->
<div class="test-section">
<h2>2. 滑块拖拽测试</h2>
<p style="color: #666; margin-bottom: 16px;">
👆 请先点击上方"生成新验证码"按钮加载图片,然后拖动滑块到背景图的正确位置。
</p>
<div style="position: relative; width: 300px; height: 150px; background: #e0e0e0; border-radius: 8px; margin-bottom: 16px; overflow: hidden;" id="slider-track">
<img id="test-bg-image" style="width: 100%; height: 100%;">
<img id="test-slider-image" style="position: absolute; top: 0; left: 0; cursor: grab; box-shadow: 0 2px 8px rgba(0,0,0,0.3);">
</div>
<div style="position: relative; width: 300px; height: 40px; background: #f0f0f0; border-radius: 20px; margin-bottom: 16px;" id="drag-track">
<div id="drag-track-bg" style="position: absolute; top: 0; left: 0; height: 100%; width: 20px; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 20px; transition: width 0.1s;"></div>
<div id="drag-thumb" style="position: absolute; top: 2px; left: 2px; width: 36px; height: 36px; background: white; border-radius: 50%; cursor: grab; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 4px rgba(0,0,0,0.2); z-index: 10;">
→
</div>
</div>
<button class="btn btn-success" onclick="verifySliderPosition()">验证滑块位置</button>
<button class="btn btn-danger" onclick="resetDragTest()">重置拖拽测试</button>
<div class="result-box" id="drag-result" style="display:none;">
<pre id="drag-result-content"></pre>
</div>
</div>
<!-- 手动验证测试 -->
<div class="test-section">
<h2>3. 手动位置验证</h2>
<div class="form-group">
<label>验证码Key</label>
<input type="text" id="verify-captcha-key" placeholder="从上方获取的captcha_key">
</div>
<div class="form-group">
<label>X位置 (0-300)</label>
<input type="number" id="verify-x-position" placeholder="拖动的X坐标" min="0" max="300">
</div>
<button class="btn" onclick="manualVerify()">提交验证</button>
<div class="result-box" id="verify-result" style="display:none;">
<pre id="verify-result-content"></pre>
</div>
</div>
<!-- 操作日志 -->
<div class="test-section">
<h2>📋 操作日志</h2>
<button class="btn" onclick="clearLogs()">清空日志</button>
<div class="log-container" id="log-container">
<div class="log-entry info">系统就绪,等待操作...</div>
</div>
</div>
</div>
<script>
// 配置
let config = {
apiBaseUrl: 'http://localhost:8000',
captchaKey: null,
correctX: null,
yPosition: null,
sliderImageWidth: 60,
trackWidth: 300,
isDragging: false,
startX: 0,
currentX: 0,
trajectory: [],
startTime: 0
};
// 日志记录
function log(message, type = 'info') {
const container = document.getElementById('log-container');
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
const timestamp = new Date().toLocaleTimeString();
entry.innerHTML = `<span class="timestamp">[${timestamp}]</span>${message}`;
container.insertBefore(entry, container.firstChild);
}
function clearLogs() {
document.getElementById('log-container').innerHTML = '<div class="log-entry info">日志已清空</div>';
}
// 更新API基础URL
document.getElementById('api-base-url').addEventListener('change', function() {
config.apiBaseUrl = this.value;
log(`API基础URL已更新为: ${config.apiBaseUrl}`);
});
// 检查服务器状态
async function checkServerStatus() {
const statusIndicator = document.getElementById('server-status');
statusIndicator.className = 'status-indicator pending';
statusIndicator.textContent = '检查中...';
try {
const response = await fetch(`${config.apiBaseUrl}/user/slider-captcha/generate/`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
statusIndicator.className = 'status-indicator success';
log('✅ 服务器连接成功', 'success');
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
statusIndicator.className = 'status-indicator error';
log(`❌ 服务器连接失败: ${error.message}`, 'error');
}
}
// 生成验证码
async function generateCaptcha() {
log('正在生成滑块验证码...', 'info');
try {
const response = await fetch(`${config.apiBaseUrl}/user/slider-captcha/generate/`);
const data = await response.json();
if (data.code === 0) {
config.captchaKey = data.data.captcha_key;
config.yPosition = data.data.y_position;
config.correctX = null; // 需要从缓存获取,但API不返回
// 显示验证码
document.getElementById('captcha-display').style.display = 'block';
document.getElementById('bg-image').src = data.data.bg_image;
document.getElementById('slider-image').src = data.data.slider_image;
document.getElementById('test-bg-image').src = data.data.bg_image;
document.getElementById('test-slider-image').src = data.data.slider_image;
// 设置滑块初始位置
document.getElementById('test-slider-image').style.top = `${data.data.y_position}px`;
document.getElementById('test-slider-image').style.left = '0px';
document.getElementById('captcha-info').textContent = `Y位置: ${data.data.y_position}px (你需要拖动滑块到背景图的灰色区块位置,X 答案由服务端保存不下发)`;
document.getElementById('captcha-key-display').textContent = `Key: ${config.captchaKey}`;
// 重置拖拽测试
resetDragTest();
// 更新手动验证表单
document.getElementById('verify-captcha-key').value = config.captchaKey;
// 显示结果
document.getElementById('generate-result').style.display = 'block';
document.getElementById('generate-result-content').textContent = JSON.stringify(data, null, 2);
log(`✅ 验证码生成成功,Key: ${config.captchaKey}`, 'success');
} else {
throw new Error(data.message || '生成失败');
}
} catch (error) {
log(`❌ 生成验证码失败: ${error.message}`, 'error');
}
}
function refreshCaptcha() {
resetDragTest();
generateCaptcha();
}
// 拖拽测试
const dragThumb = document.getElementById('drag-thumb');
const dragTrack = document.getElementById('drag-track');
const dragTrackBg = document.getElementById('drag-track-bg');
const testSlider = document.getElementById('test-slider-image');
dragThumb.addEventListener('mousedown', startDrag);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', endDrag);
// 触摸支持
dragThumb.addEventListener('touchstart', startDrag);
document.addEventListener('touchmove', drag);
document.addEventListener('touchend', endDrag);
function startDrag(e) {
e.preventDefault();
config.isDragging = true;
config.startTime = Date.now();
config.trajectory = [];
config.currentX = parseInt(dragThumb.style.left) || 0;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
config.startX = clientX - config.currentX;
dragThumb.style.cursor = 'grabbing';
log('开始拖动滑块...', 'info');
}
function drag(e) {
if (!config.isDragging) return;
e.preventDefault();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const trackRect = dragTrack.getBoundingClientRect();
let x = clientX - config.startX - trackRect.left;
x = Math.max(0, Math.min(x, trackRect.width - 40));
config.currentX = x;
dragThumb.style.left = `${x}px`;
dragTrackBg.style.width = `${x + 20}px`;
// 同步移动测试图片上的滑块
const scale = config.trackWidth / trackRect.width;
const imageX = Math.round(x * scale);
testSlider.style.left = `${imageX}px`;
// 记录轨迹
config.trajectory.push({
x: Math.round(x),
y: e.touches ? e.touches[0].clientY : e.clientY,
t: Date.now() - config.startTime
});
}
function endDrag(e) {
if (!config.isDragging) return;
config.isDragging = false;
dragThumb.style.cursor = 'grab';
const scale = config.trackWidth / (dragTrack.getBoundingClientRect().width - 40);
const finalX = Math.round(config.currentX * scale);
log(`拖动结束,最终位置: ${finalX}px,轨迹点数: ${config.trajectory.length}`, 'info');
// 更新手动验证表单
document.getElementById('verify-x-position').value = finalX;
}
function resetDragTest() {
config.currentX = 0;
config.trajectory = [];
dragThumb.style.left = '2px';
dragTrackBg.style.width = '20px';
testSlider.style.left = '0px';
log('拖拽测试已重置', 'info');
}
// 验证滑块位置(简化版,不传轨迹)
async function verifySliderPosition() {
if (!config.captchaKey) {
log('❌ 请先生成验证码', 'error');
return;
}
const scale = config.trackWidth / (dragTrack.getBoundingClientRect().width - 40);
const xPosition = Math.round(config.currentX * scale);
log(`正在验证滑块位置: ${xPosition}px`, 'info');
try {
const response = await fetch(`${config.apiBaseUrl}/user/slider-captcha/verify/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
captcha_key: config.captchaKey,
x_position: xPosition
})
});
const data = await response.json();
document.getElementById('drag-result').style.display = 'block';
document.getElementById('drag-result-content').textContent = JSON.stringify(data, null, 2);
if (data.code === 0 && data.data.verified) {
log('✅ 滑块验证成功!', 'success');
} else {
log(`❌ 滑块验证失败: ${data.message}`, 'error');
}
} catch (error) {
log(`❌ 验证请求失败: ${error.message}`, 'error');
}
}
// 手动验证(带轨迹)
async function manualVerify() {
const captchaKey = document.getElementById('verify-captcha-key').value.trim();
const xPosition = parseInt(document.getElementById('verify-x-position').value);
if (!captchaKey || isNaN(xPosition)) {
log('❌ 请填写验证码Key和X位置', 'error');
return;
}
log(`正在手动验证: Key=${captchaKey}, X=${xPosition}`, 'info');
try {
const response = await fetch(`${config.apiBaseUrl}/user/slider-captcha/verify/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
captcha_key: captchaKey,
x_position: xPosition,
trajectory: config.trajectory.length > 0 ? config.trajectory : undefined
})
});
const data = await response.json();
document.getElementById('verify-result').style.display = 'block';
document.getElementById('verify-result-content').textContent = JSON.stringify(data, null, 2);
if (data.code === 0 && data.data.verified) {
log('✅ 手动验证成功!', 'success');
} else {
log(`❌ 手动验证失败: ${data.message}`, 'error');
}
} catch (error) {
log(`❌ 验证请求失败: ${error.message}`, 'error');
}
}
// 页面加载时自动检查服务器状态
window.addEventListener('load', function() {
log('页面加载完成,等待用户操作...', 'info');
checkServerStatus();
});
</script>
</body>
</html>