补充漏推送的东西
This commit is contained in:
@@ -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);
|
||||
@@ -86,3 +105,8 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('refreshCaptcha').addEventListener('click', async () => {
|
||||
needCaptcha = true;
|
||||
await loadCaptcha();
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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']}"})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user