125 lines
3.6 KiB
JavaScript
125 lines
3.6 KiB
JavaScript
// Utility: read cookie value
|
|
function getCookie(name) {
|
|
const value = `; ${document.cookie}`;
|
|
const parts = value.split(`; ${name}=`);
|
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
|
}
|
|
|
|
// Convert base64 string to ArrayBuffer
|
|
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;
|
|
}
|
|
|
|
// ArrayBuffer to base64
|
|
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 deriveKey(password, saltBytes, iterations = 100000, length = 32) {
|
|
const encoder = new TextEncoder();
|
|
const keyMaterial = await window.crypto.subtle.importKey(
|
|
'raw',
|
|
encoder.encode(password),
|
|
{ name: 'PBKDF2' },
|
|
false,
|
|
['deriveBits']
|
|
);
|
|
|
|
const derivedBits = await window.crypto.subtle.deriveBits(
|
|
{
|
|
name: 'PBKDF2',
|
|
salt: saltBytes,
|
|
iterations,
|
|
hash: 'SHA-256'
|
|
},
|
|
keyMaterial,
|
|
length * 8
|
|
);
|
|
|
|
return new Uint8Array(derivedBits);
|
|
}
|
|
|
|
async function hmacSha256(keyBytes, messageBytes) {
|
|
const key = await window.crypto.subtle.importKey(
|
|
'raw',
|
|
keyBytes,
|
|
{ name: 'HMAC', hash: { name: 'SHA-256' } },
|
|
false,
|
|
['sign']
|
|
);
|
|
const signature = await window.crypto.subtle.sign('HMAC', key, messageBytes);
|
|
return new Uint8Array(signature);
|
|
}
|
|
|
|
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 {
|
|
// Step 1: get challenge (nonce + salt)
|
|
const csrftoken = getCookie('csrftoken');
|
|
const chalResp = await fetch('/accounts/challenge/', {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrftoken || ''
|
|
},
|
|
body: JSON.stringify({ username })
|
|
});
|
|
if (!chalResp.ok) {
|
|
throw new Error('获取挑战失败');
|
|
}
|
|
const chal = await chalResp.json();
|
|
const nonceBytes = new Uint8Array(base64ToArrayBuffer(chal.nonce));
|
|
const saltBytes = new Uint8Array(base64ToArrayBuffer(chal.salt));
|
|
|
|
// Step 2: derive secret and compute HMAC
|
|
const derived = await deriveKey(password, saltBytes, 100000, 32);
|
|
const hmac = await hmacSha256(derived, nonceBytes);
|
|
const hmacB64 = arrayBufferToBase64(hmac);
|
|
|
|
// Step 3: submit login with username and hmac
|
|
const submitResp = await fetch('/accounts/login/submit/', {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrftoken || ''
|
|
},
|
|
body: JSON.stringify({ username, hmac: hmacB64 })
|
|
});
|
|
const submitJson = await submitResp.json();
|
|
if (!submitResp.ok || !submitJson.ok) {
|
|
throw new Error(submitJson.message || '登录失败');
|
|
}
|
|
// Redirect to home with user_id
|
|
window.location.href = submitJson.redirect_url;
|
|
} catch (err) {
|
|
console.error(err);
|
|
errorEl.textContent = err.message || '发生错误';
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
}); |