- 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)
533 lines
20 KiB
HTML
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>
|