补充漏推送的东西

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); 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) => { document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const errorEl = document.getElementById('error'); const errorEl = document.getElementById('error');
@@ -68,7 +82,9 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => {
const aesKey = await importAesKey(aesKeyRaw); const aesKey = await importAesKey(aesKeyRaw);
const iv = new Uint8Array(12); window.crypto.getRandomValues(iv); 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 ct = await aesGcmEncrypt(aesKey, iv, payload);
const ctB64 = arrayBufferToBase64(ct); const ctB64 = arrayBufferToBase64(ct);
const ivB64 = arrayBufferToBase64(iv); 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 }) 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(); 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; window.location.href = submitJson.redirect_url;
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -85,4 +104,9 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => {
} finally { } finally {
btn.disabled = false; btn.disabled = false;
} }
});
document.getElementById('refreshCaptcha').addEventListener('click', async () => {
needCaptcha = true;
await loadCaptcha();
}); });

View File

@@ -30,6 +30,15 @@
<label for="password">密码</label> <label for="password">密码</label>
<input id="password" name="password" type="password" autocomplete="current-password" required /> <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> <button id="loginBtn" type="submit">登录</button>
<div id="error" class="error"></div> <div id="error" class="error"></div>
</form> </form>

View File

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

View File

@@ -1,6 +1,9 @@
import base64 import base64
import json import json
import os import os
import io
import random
import string
from django.http import JsonResponse, HttpResponseBadRequest from django.http import JsonResponse, HttpResponseBadRequest
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
@@ -24,6 +27,22 @@ def pubkey(request):
pk_b64 = get_public_key_spki_b64() pk_b64 = get_public_key_spki_b64()
return JsonResponse({"public_key_spki": pk_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"]) @require_http_methods(["POST"])
@csrf_protect @csrf_protect
@@ -66,11 +85,18 @@ def secure_login_submit(request):
password = (obj.get("password") or "") password = (obj.get("password") or "")
if not username or not password: if not username or not password:
return HttpResponseBadRequest("Missing credentials") 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) user = get_user_by_username(username)
if not user: 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 ""): 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: try:
request.session.cycle_key() request.session.cycle_key()
except Exception: except Exception:
@@ -83,6 +109,10 @@ def secure_login_submit(request):
request.session["permission"] = 1 request.session["permission"] = 1
if "session_enc_key_b64" in request.session: if "session_enc_key_b64" in request.session:
del request.session["session_enc_key_b64"] 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']}"}) return JsonResponse({"ok": True, "redirect_url": f"/main/home/?user_id={user['user_id']}"})