补充漏推送的东西
This commit is contained in:
@@ -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();
|
||||||
});
|
});
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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']}"})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user