Files
Achievement_Inputing/accounts/static/accounts/login.js

136 lines
5.6 KiB
JavaScript

function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
function base64ToArrayBuffer(b64) {
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes.buffer;
}
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary);
}
async function importRsaPublicKey(spkiBytes) {
return window.crypto.subtle.importKey('spki', spkiBytes, { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['encrypt']);
}
async function rsaOaepEncrypt(publicKey, dataBytes) {
const encrypted = await window.crypto.subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, dataBytes);
return new Uint8Array(encrypted);
}
async function importAesKey(keyBytes) {
return window.crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']);
}
async function aesGcmEncrypt(aesKey, ivBytes, dataBytes) {
const ct = await window.crypto.subtle.encrypt({ name: 'AES-GCM', iv: ivBytes }, aesKey, 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');
errorEl.textContent = '';
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
if (!username || !password) { errorEl.textContent = '请输入账户与密码'; return; }
const btn = document.getElementById('loginBtn');
btn.disabled = true;
try {
const csrftoken = getCookie('csrftoken');
const pkResp = await fetch('/accounts/pubkey/', { method: 'GET', credentials: 'same-origin', headers: { 'X-CSRFToken': csrftoken || '' } });
if (!pkResp.ok) throw new Error('获取公钥失败');
const pkJson = await pkResp.json();
const spkiBytes = new Uint8Array(base64ToArrayBuffer(pkJson.public_key_spki));
const pubKey = await importRsaPublicKey(spkiBytes);
const aesKeyRaw = new Uint8Array(32); window.crypto.getRandomValues(aesKeyRaw);
const encAesKey = await rsaOaepEncrypt(pubKey, aesKeyRaw);
const encAesKeyB64 = arrayBufferToBase64(encAesKey);
const setKeyResp = await fetch('/accounts/session-key/', {
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' }, body: JSON.stringify({ encrypted_key: encAesKeyB64 })
});
const setKeySnapshot = await (async () => {
const clone = setKeyResp.clone();
const txt = await clone.text();
let parsed = null;
try { parsed = await setKeyResp.json(); } catch (_) {}
return { txt, parsed };
})();
if (!setKeySnapshot.parsed) {
const msg = (setKeySnapshot.txt || '').trim();
const mapped = msg.toLowerCase().includes('decrypt error') ? '会话密钥解密失败,请刷新页面后重试' : (msg || '设置会话密钥失败');
throw new Error(mapped);
}
const setKeyJson = setKeySnapshot.parsed;
if (!setKeyResp.ok || !setKeyJson.ok) throw new Error(setKeyJson.message || '设置会话密钥失败');
const aesKey = await importAesKey(aesKeyRaw);
const iv = new Uint8Array(12); window.crypto.getRandomValues(iv);
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);
const submitResp = await fetch('/accounts/login/secure-submit/', {
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' }, body: JSON.stringify({ iv: ivB64, ciphertext: ctB64 })
});
const submitSnapshot = await (async () => {
const clone = submitResp.clone();
const txt = await clone.text();
let parsed = null;
try { parsed = await submitResp.json(); } catch (_) {}
return { txt, parsed };
})();
if (!submitSnapshot.parsed) {
const msg = (submitSnapshot.txt || '').trim();
const mapped = msg.toLowerCase().includes('decrypt error') ? '解密失败,请刷新页面后重试' : (msg || '服务器响应异常');
throw new Error(mapped);
}
const submitJson = submitSnapshot.parsed;
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);
errorEl.textContent = err.message || '发生错误';
} finally {
btn.disabled = false;
}
});
document.getElementById('refreshCaptcha').addEventListener('click', async () => {
needCaptcha = true;
await loadCaptcha();
});