Files
zsglpt/templates/login.html
yuyx de51e1b7c7 修复多项安全漏洞和Bug
1. 安全修复:
   - 修复密码重置接口用户枚举漏洞,统一返回消息防止信息泄露
   - 统一密码强度验证为8位以上且包含字母和数字
   - 添加第三方账号密码加密存储(Fernet对称加密)
   - 修复默认管理员弱密码问题,改用随机生成强密码
   - 修复管理员回复XSS漏洞,添加HTML转义
   - 将MD5哈希替换为SHA256

2. 并发Bug修复:
   - 修复日志缓存竞态条件,添加锁保护
   - 修复截图信号量配置变更后不生效问题

3. 其他改进:
   - 添加API参数类型验证和边界检查
   - 新增crypto_utils.py加密工具模块

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 19:14:14 +08:00

262 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>
:root {
--md-primary: #1976D2;
--md-primary-dark: #1565C0;
--md-primary-light: #BBDEFB;
--md-background: #FAFAFA;
--md-surface: #FFFFFF;
--md-error: #B00020;
--md-success: #4CAF50;
--md-on-primary: #FFFFFF;
--md-on-surface: #212121;
--md-on-surface-medium: #666666;
--md-shadow-lg: 0 8px 30px rgba(0,0,0,0.12);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.login-card {
background: var(--md-surface);
border-radius: 16px;
box-shadow: var(--md-shadow-lg);
width: 100%;
max-width: 400px;
overflow: hidden;
}
.login-header {
background: var(--md-primary);
color: var(--md-on-primary);
padding: 32px 24px;
text-align: center;
}
.login-header .logo { font-size: 48px; margin-bottom: 12px; }
.login-header h1 { font-size: 24px; font-weight: 500; margin-bottom: 4px; }
.login-header p { font-size: 14px; opacity: 0.9; }
.login-body { padding: 32px 24px; }
.form-group { margin-bottom: 24px; }
.form-group label { display: block; font-size: 14px; font-weight: 500; color: var(--md-on-surface-medium); margin-bottom: 8px; }
.form-group input {
width: 100%;
padding: 14px 16px;
border: 2px solid #E0E0E0;
border-radius: 8px;
font-size: 16px;
transition: all 0.2s;
background: #FAFAFA;
}
.form-group input:focus {
outline: none;
border-color: var(--md-primary);
background: var(--md-surface);
box-shadow: 0 0 0 3px var(--md-primary-light);
}
.captcha-row { display: flex; gap: 12px; align-items: center; }
.captcha-row input { flex: 1; }
.captcha-code {
font-size: 20px;
font-weight: bold;
letter-spacing: 4px;
color: var(--md-primary);
padding: 10px 16px;
background: var(--md-primary-light);
border-radius: 8px;
user-select: none;
}
.captcha-refresh {
padding: 10px 16px;
background: #F5F5F5;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 18px;
}
.captcha-refresh:hover { background: #EEEEEE; }
.forgot-link { text-align: right; margin-top: -16px; margin-bottom: 24px; }
.forgot-link a { color: var(--md-primary); text-decoration: none; font-size: 14px; font-weight: 500; }
.forgot-link a:hover { text-decoration: underline; }
.btn-login {
width: 100%;
padding: 16px;
background: var(--md-primary);
color: var(--md-on-primary);
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-login:hover { background: var(--md-primary-dark); box-shadow: 0 4px 12px rgba(25, 118, 210, 0.4); }
.register-link {
text-align: center;
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #E0E0E0;
color: var(--md-on-surface-medium);
font-size: 14px;
}
.register-link a { color: var(--md-primary); text-decoration: none; font-weight: 600; }
.register-link a:hover { text-decoration: underline; }
.message { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; display: none; }
.message.error { background: #FFEBEE; color: var(--md-error); border: 1px solid #FFCDD2; }
.message.success { background: #E8F5E9; color: var(--md-success); border: 1px solid #C8E6C9; }
.modal-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: flex; justify-content: center; align-items: center;
opacity: 0; visibility: hidden; transition: all 0.3s; z-index: 1000; padding: 20px;
}
.modal-overlay.active { opacity: 1; visibility: visible; }
.modal {
background: var(--md-surface);
border-radius: 16px;
width: 100%; max-width: 400px;
box-shadow: var(--md-shadow-lg);
transform: translateY(-20px); transition: transform 0.3s;
}
.modal-overlay.active .modal { transform: translateY(0); }
.modal-header { padding: 24px; border-bottom: 1px solid #E0E0E0; }
.modal-header h2 { font-size: 20px; font-weight: 500; margin-bottom: 4px; }
.modal-header p { font-size: 14px; color: var(--md-on-surface-medium); }
.modal-body { padding: 24px; }
.modal-footer { padding: 16px 24px; border-top: 1px solid #E0E0E0; display: flex; gap: 12px; justify-content: flex-end; }
.btn-secondary { padding: 12px 24px; background: #F5F5F5; color: var(--md-on-surface); border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
.btn-secondary:hover { background: #EEEEEE; }
.btn-primary { padding: 12px 24px; background: var(--md-primary); color: var(--md-on-primary); border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; }
.btn-primary:hover { background: var(--md-primary-dark); }
@media (max-width: 480px) {
body { padding: 12px; }
.login-card { max-width: 100%; }
.login-header { padding: 24px 20px; }
.login-header .logo { font-size: 40px; margin-bottom: 10px; }
.login-header h1 { font-size: 20px; }
.login-header p { font-size: 13px; }
.login-body { padding: 24px 20px; }
.form-group { margin-bottom: 20px; }
.form-group label { font-size: 13px; }
.form-group input { padding: 12px 14px; font-size: 16px; } /* iOS防止自动缩放 */
.captcha-code { padding: 8px 12px; font-size: 18px; letter-spacing: 3px; }
.captcha-refresh { padding: 8px 12px; font-size: 16px; }
.btn-login { padding: 14px; font-size: 15px; }
.modal { max-width: 100%; }
.modal-header, .modal-body { padding: 20px; }
.modal-header h2 { font-size: 18px; }
.modal-footer { padding: 14px 20px; flex-direction: column; }
.modal-footer button { width: 100%; }
}
</style>
</head>
<body>
<div class="login-card">
<div class="login-header">
<div class="logo">📚</div>
<h1>知识管理平台</h1>
<p>自动化浏览学习内容</p>
</div>
<div class="login-body">
<div id="errorMessage" class="message error"></div>
<div id="successMessage" class="message success"></div>
<form id="loginForm" onsubmit="handleLogin(event)">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" placeholder="请输入密码" required>
</div>
<div id="captchaGroup" class="form-group" style="display: none;">
<label for="captcha">验证码</label>
<div class="captcha-row">
<input type="text" id="captcha" name="captcha" placeholder="请输入验证码">
<img id="captchaImage" src="" alt="验证码" style="height: 36px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;" onclick="refreshCaptcha()" title="点击刷新">
<button type="button" class="captcha-refresh" onclick="refreshCaptcha()">🔄</button>
</div>
</div>
<div class="forgot-link"><a href="#" onclick="showForgotPassword(event)">忘记密码?</a></div>
<button type="submit" class="btn-login">登 录</button>
</form>
<div class="register-link">还没有账号? <a href="/register">立即注册</a></div>
</div>
</div>
<div id="forgotPasswordModal" class="modal-overlay" onclick="if(event.target===this)closeForgotPassword()">
<div class="modal">
<div class="modal-header"><h2>重置密码</h2><p>填写信息后等待管理员审核</p></div>
<div class="modal-body">
<div id="modalErrorMessage" class="message error"></div>
<div id="modalSuccessMessage" class="message success"></div>
<form id="resetPasswordForm" onsubmit="handleResetPassword(event)">
<div class="form-group"><label>用户名</label><input type="text" id="resetUsername" placeholder="请输入用户名" required></div>
<div class="form-group"><label>邮箱(可选)</label><input type="email" id="resetEmail" placeholder="用于验证身份"></div>
<div class="form-group"><label>新密码</label><input type="password" id="resetNewPassword" placeholder="至少8位包含字母和数字" required></div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn-secondary" onclick="closeForgotPassword()">取消</button>
<button type="button" class="btn-primary" onclick="document.getElementById('resetPasswordForm').dispatchEvent(new Event('submit'))">提交申请</button>
</div>
</div>
</div>
<script>
let captchaSession = '';
let needCaptcha = false;
async function handleLogin(event) {
event.preventDefault();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value.trim();
const captcha = document.getElementById('captcha') ? document.getElementById('captcha').value.trim() : '';
const errorDiv = document.getElementById('errorMessage');
const successDiv = document.getElementById('successMessage');
errorDiv.style.display = 'none';
successDiv.style.display = 'none';
if (!username || !password) { errorDiv.textContent = '用户名和密码不能为空'; errorDiv.style.display = 'block'; return; }
if (needCaptcha && !captcha) { errorDiv.textContent = '请输入验证码'; errorDiv.style.display = 'block'; return; }
try {
const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password, captcha_session: captchaSession, captcha, need_captcha: needCaptcha }) });
const data = await response.json();
if (response.ok) { successDiv.textContent = '登录成功,正在跳转...'; successDiv.style.display = 'block'; setTimeout(() => { window.location.href = '/app'; }, 500); }
else { errorDiv.textContent = data.error || '登录失败'; errorDiv.style.display = 'block'; if (data.need_captcha) { needCaptcha = true; document.getElementById('captchaGroup').style.display = 'block'; await generateCaptcha(); } }
} catch (error) { errorDiv.textContent = '网络错误'; errorDiv.style.display = 'block'; }
}
function showForgotPassword(event) { event.preventDefault(); document.getElementById('forgotPasswordModal').classList.add('active'); }
function closeForgotPassword() { document.getElementById('forgotPasswordModal').classList.remove('active'); document.getElementById('resetPasswordForm').reset(); document.getElementById('modalErrorMessage').style.display = 'none'; document.getElementById('modalSuccessMessage').style.display = 'none'; }
async function handleResetPassword(event) {
event.preventDefault();
const username = document.getElementById('resetUsername').value.trim();
const email = document.getElementById('resetEmail').value.trim();
const newPassword = document.getElementById('resetNewPassword').value.trim();
const errorDiv = document.getElementById('modalErrorMessage');
const successDiv = document.getElementById('modalSuccessMessage');
errorDiv.style.display = 'none'; successDiv.style.display = 'none';
if (!username || !newPassword) { errorDiv.textContent = '用户名和新密码不能为空'; errorDiv.style.display = 'block'; return; }
if (newPassword.length < 8) { errorDiv.textContent = '密码长度至少8位'; errorDiv.style.display = 'block'; return; }
if (!/[a-zA-Z]/.test(newPassword) || !/\d/.test(newPassword)) { errorDiv.textContent = '密码必须包含字母和数字'; errorDiv.style.display = 'block'; return; }
try {
const response = await fetch('/api/reset_password_request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, email, new_password: newPassword }) });
const data = await response.json();
if (response.ok) { successDiv.textContent = '申请已提交,请等待审核'; successDiv.style.display = 'block'; setTimeout(closeForgotPassword, 2000); }
else { errorDiv.textContent = data.error || '申请失败'; errorDiv.style.display = 'block'; }
} catch (error) { errorDiv.textContent = '网络错误'; errorDiv.style.display = 'block'; }
}
async function generateCaptcha() { try { const response = await fetch('/api/generate_captcha', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.session_id && data.captcha_image) { captchaSession = data.session_id; document.getElementById('captchaImage').src = data.captcha_image; } } catch (error) { console.error('生成验证码失败:', error); } }
async function refreshCaptcha() { await generateCaptcha(); document.getElementById('captcha').value = ''; }
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeForgotPassword(); });
</script>
</body>
</html>