补充漏推送的东西

This commit is contained in:
2025-11-17 16:22:47 +08:00
parent f93286a5fe
commit 1392275337
4 changed files with 68 additions and 4 deletions

View File

@@ -36,6 +36,20 @@ async function aesGcmEncrypt(aesKey, ivBytes, dataBytes) {
return new Uint8Array(ct);
}
let needCaptcha = false;
async function loadCaptcha() {
const csrftoken = getCookie('csrftoken');
const resp = await fetch('/accounts/captcha/', { method: 'GET', credentials: 'same-origin', headers: { 'X-CSRFToken': csrftoken || '' } });
const data = await resp.json();
if (resp.ok && data.ok) {
const img = document.getElementById('captchaImg');
const box = document.getElementById('captchaBox');
img.src = 'data:image/png;base64,' + data.image_b64;
box.style.display = 'block';
}
}
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const errorEl = document.getElementById('error');
@@ -68,7 +82,9 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => {
const aesKey = await importAesKey(aesKeyRaw);
const iv = new Uint8Array(12); window.crypto.getRandomValues(iv);
const payload = new TextEncoder().encode(JSON.stringify({ username, password }));
const obj = { username, password };
if (needCaptcha) obj.captcha = (document.getElementById('captcha').value || '').trim();
const payload = new TextEncoder().encode(JSON.stringify(obj));
const ct = await aesGcmEncrypt(aesKey, iv, payload);
const ctB64 = arrayBufferToBase64(ct);
const ivB64 = arrayBufferToBase64(iv);
@@ -77,7 +93,10 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => {
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' }, body: JSON.stringify({ iv: ivB64, ciphertext: ctB64 })
});
const submitJson = await submitResp.json();
if (!submitResp.ok || !submitJson.ok) throw new Error(submitJson.message || '登录失败');
if (!submitResp.ok || !submitJson.ok) {
if (submitJson && submitJson.captcha_required) { needCaptcha = true; await loadCaptcha(); }
throw new Error(submitJson.message || '登录失败');
}
window.location.href = submitJson.redirect_url;
} catch (err) {
console.error(err);
@@ -85,4 +104,9 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => {
} finally {
btn.disabled = false;
}
});
document.getElementById('refreshCaptcha').addEventListener('click', async () => {
needCaptcha = true;
await loadCaptcha();
});

View File

@@ -30,6 +30,15 @@
<label for="password">密码</label>
<input id="password" name="password" type="password" autocomplete="current-password" required />
<div id="captchaBox" style="display:none; margin-top:12px;">
<label for="captcha">验证码</label>
<div style="display:flex; gap:8px; align-items:center;">
<input id="captcha" name="captcha" type="text" autocomplete="off" style="flex:1;" />
<img id="captchaImg" alt="验证码" style="height:40px; border:1px solid #dcdde1; border-radius:6px;" />
<button id="refreshCaptcha" type="button" style="width:auto;">刷新</button>
</div>
</div>
<button id="loginBtn" type="submit">登录</button>
<div id="error" class="error"></div>
</form>

View File

@@ -5,6 +5,7 @@ app_name = "accounts"
urlpatterns = [
path("login/", views.login_page, name="login"),
path("pubkey/", views.pubkey, name="pubkey"),
path("captcha/", views.captcha, name="captcha"),
path("session-key/", views.set_session_key, name="set_session_key"),
path("login/secure-submit/", views.secure_login_submit, name="secure_login_submit"),
path("logout/", views.logout, name="logout"),

View File

@@ -1,6 +1,9 @@
import base64
import json
import os
import io
import random
import string
from django.http import JsonResponse, HttpResponseBadRequest
from django.shortcuts import render, redirect
@@ -24,6 +27,22 @@ def pubkey(request):
pk_b64 = get_public_key_spki_b64()
return JsonResponse({"public_key_spki": pk_b64})
@require_http_methods(["GET"])
@ensure_csrf_cookie
def captcha(request):
try:
from captcha.image import ImageCaptcha
except Exception:
return JsonResponse({"ok": False, "message": "captcha unavailable"}, status=500)
code = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(5))
request.session["captcha_code"] = code
img = ImageCaptcha(width=160, height=60)
image = img.generate_image(code)
buf = io.BytesIO()
image.save(buf, format="PNG")
b64 = base64.b64encode(buf.getvalue()).decode("ascii")
return JsonResponse({"ok": True, "image_b64": b64})
@require_http_methods(["POST"])
@csrf_protect
@@ -66,11 +85,18 @@ def secure_login_submit(request):
password = (obj.get("password") or "")
if not username or not password:
return HttpResponseBadRequest("Missing credentials")
if bool(request.session.get("login_failed_once")):
ans = (obj.get("captcha") or "").strip()
code = request.session.get("captcha_code")
if not ans or not code or ans.lower() != str(code).lower():
return JsonResponse({"ok": False, "message": "验证码错误", "captcha_required": True}, status=401)
user = get_user_by_username(username)
if not user:
return JsonResponse({"ok": False, "message": "User not found"}, status=401)
request.session["login_failed_once"] = True
return JsonResponse({"ok": False, "message": "用户不存在", "captcha_required": True}, status=401)
if not verify_password(password, user.get("password_salt") or "", user.get("password_hash") or ""):
return JsonResponse({"ok": False, "message": "Invalid credentials"}, status=401)
request.session["login_failed_once"] = True
return JsonResponse({"ok": False, "message": "账户或密码错误", "captcha_required": True}, status=401)
try:
request.session.cycle_key()
except Exception:
@@ -83,6 +109,10 @@ def secure_login_submit(request):
request.session["permission"] = 1
if "session_enc_key_b64" in request.session:
del request.session["session_enc_key_b64"]
if "login_failed_once" in request.session:
del request.session["login_failed_once"]
if "captcha_code" in request.session:
del request.session["captcha_code"]
return JsonResponse({"ok": True, "redirect_url": f"/main/home/?user_id={user['user_id']}"})