修改登录逻辑,使用RSA-OAEP 包裹每会话独立 AES-GCM 密钥 + 加密提交凭据
This commit is contained in:
@@ -1,5 +1,23 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
||||||
|
from cryptography.hazmat.primitives import serialization, hashes
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
except Exception:
|
||||||
|
rsa = None
|
||||||
|
padding = None
|
||||||
|
serialization = None
|
||||||
|
hashes = None
|
||||||
|
Cipher = None
|
||||||
|
algorithms = None
|
||||||
|
modes = None
|
||||||
|
default_backend = None
|
||||||
|
|
||||||
|
|
||||||
def salt_for_username(username: str) -> bytes:
|
def salt_for_username(username: str) -> bytes:
|
||||||
@@ -17,4 +35,59 @@ def derive_password(password_plain: str, salt: bytes, iterations: int = 100_000,
|
|||||||
|
|
||||||
def hmac_sha256(key: bytes, message: bytes) -> bytes:
|
def hmac_sha256(key: bytes, message: bytes) -> bytes:
|
||||||
"""Compute HMAC-SHA256 signature for the given message using key bytes."""
|
"""Compute HMAC-SHA256 signature for the given message using key bytes."""
|
||||||
return hmac.new(key, message, hashlib.sha256).digest()
|
return hmac.new(key, message, hashlib.sha256).digest()
|
||||||
|
|
||||||
|
|
||||||
|
_RSA_PRIVATE = None
|
||||||
|
_RSA_PUBLIC = None
|
||||||
|
|
||||||
|
def _ensure_rsa_keys():
|
||||||
|
global _RSA_PRIVATE, _RSA_PUBLIC
|
||||||
|
if _RSA_PRIVATE is None:
|
||||||
|
if rsa is None:
|
||||||
|
raise RuntimeError("cryptography library is required for RSA operations")
|
||||||
|
_RSA_PRIVATE = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||||
|
_RSA_PUBLIC = _RSA_PRIVATE.public_key()
|
||||||
|
|
||||||
|
def get_public_key_spki_b64() -> str:
|
||||||
|
_ensure_rsa_keys()
|
||||||
|
spki = _RSA_PUBLIC.public_bytes(encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo)
|
||||||
|
return base64.b64encode(spki).decode('ascii')
|
||||||
|
|
||||||
|
def rsa_oaep_decrypt_b64(ciphertext_b64: str) -> bytes:
|
||||||
|
_ensure_rsa_keys()
|
||||||
|
ct = base64.b64decode(ciphertext_b64)
|
||||||
|
return _RSA_PRIVATE.decrypt(ct, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
|
||||||
|
|
||||||
|
def aes_gcm_decrypt_b64(key_bytes: bytes, iv_b64: str, ciphertext_b64: str) -> bytes:
|
||||||
|
if Cipher is None:
|
||||||
|
raise RuntimeError("cryptography library is required for AES operations")
|
||||||
|
iv = base64.b64decode(iv_b64)
|
||||||
|
data = base64.b64decode(ciphertext_b64)
|
||||||
|
if len(data) < 16:
|
||||||
|
raise ValueError("ciphertext too short")
|
||||||
|
ct = data[:-16]
|
||||||
|
tag = data[-16:]
|
||||||
|
decryptor = Cipher(algorithms.AES(key_bytes), modes.GCM(iv, tag), backend=default_backend()).decryptor()
|
||||||
|
pt = decryptor.update(ct) + decryptor.finalize()
|
||||||
|
return pt
|
||||||
|
|
||||||
|
def gen_salt(length: int = 16) -> bytes:
|
||||||
|
return os.urandom(length)
|
||||||
|
|
||||||
|
def hash_password_with_salt(password_plain: str, salt: bytes, iterations: int = 200_000, dklen: int = 32) -> bytes:
|
||||||
|
return hashlib.pbkdf2_hmac('sha256', password_plain.encode('utf-8'), salt, iterations, dklen=dklen)
|
||||||
|
|
||||||
|
def hash_password_random_salt(password_plain: str) -> Tuple[str, str]:
|
||||||
|
salt = gen_salt(16)
|
||||||
|
h = hash_password_with_salt(password_plain, salt)
|
||||||
|
return base64.b64encode(salt).decode('ascii'), base64.b64encode(h).decode('ascii')
|
||||||
|
|
||||||
|
def verify_password(password_plain: str, salt_b64: str, hash_b64: str) -> bool:
|
||||||
|
try:
|
||||||
|
salt = base64.b64decode(salt_b64)
|
||||||
|
expected = base64.b64decode(hash_b64)
|
||||||
|
actual = hash_password_with_salt(password_plain, salt)
|
||||||
|
return hmac.compare_digest(actual, expected)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
@@ -1,19 +1,14 @@
|
|||||||
import base64
|
import base64
|
||||||
from elastic.es_connect import get_user_by_username as es_get_user_by_username
|
from elastic.es_connect import get_user_by_username as es_get_user_by_username
|
||||||
from .crypto import salt_for_username, derive_password
|
|
||||||
|
|
||||||
def get_user_by_username(username: str):
|
def get_user_by_username(username: str):
|
||||||
"""
|
|
||||||
期望ES中存储的是明文密码,登录时按用户名盐派生后对nonce做HMAC验证。
|
|
||||||
"""
|
|
||||||
es_user = es_get_user_by_username(username)
|
es_user = es_get_user_by_username(username)
|
||||||
if es_user:
|
if es_user:
|
||||||
salt = salt_for_username(username)
|
|
||||||
derived = derive_password(es_user.get('password', ''), salt)
|
|
||||||
return {
|
return {
|
||||||
'user_id': es_user.get('user_id', 0),
|
'user_id': es_user.get('user_id', 0),
|
||||||
'username': es_user.get('username', ''),
|
'username': es_user.get('username', ''),
|
||||||
'password': base64.b64encode(derived).decode('ascii'),
|
'password_hash': es_user.get('password_hash'),
|
||||||
|
'password_salt': es_user.get('password_salt'),
|
||||||
'permission': es_user.get('permission', 1),
|
'permission': es_user.get('permission', 1),
|
||||||
}
|
}
|
||||||
return None
|
return None
|
||||||
@@ -1,64 +1,39 @@
|
|||||||
// Utility: read cookie value
|
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
const value = `; ${document.cookie}`;
|
const value = `; ${document.cookie}`;
|
||||||
const parts = value.split(`; ${name}=`);
|
const parts = value.split(`; ${name}=`);
|
||||||
if (parts.length === 2) return parts.pop().split(';').shift();
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert base64 string to ArrayBuffer
|
|
||||||
function base64ToArrayBuffer(b64) {
|
function base64ToArrayBuffer(b64) {
|
||||||
const binary = atob(b64);
|
const binary = atob(b64);
|
||||||
const bytes = new Uint8Array(binary.length);
|
const bytes = new Uint8Array(binary.length);
|
||||||
for (let i = 0; i < binary.length; i++) {
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||||
bytes[i] = binary.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return bytes.buffer;
|
return bytes.buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArrayBuffer to base64
|
|
||||||
function arrayBufferToBase64(buffer) {
|
function arrayBufferToBase64(buffer) {
|
||||||
const bytes = new Uint8Array(buffer);
|
const bytes = new Uint8Array(buffer);
|
||||||
let binary = '';
|
let binary = '';
|
||||||
for (let i = 0; i < bytes.byteLength; i++) {
|
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
|
||||||
binary += String.fromCharCode(bytes[i]);
|
|
||||||
}
|
|
||||||
return btoa(binary);
|
return btoa(binary);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deriveKey(password, saltBytes, iterations = 100000, length = 32) {
|
async function importRsaPublicKey(spkiBytes) {
|
||||||
const encoder = new TextEncoder();
|
return window.crypto.subtle.importKey('spki', spkiBytes, { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['encrypt']);
|
||||||
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) {
|
async function rsaOaepEncrypt(publicKey, dataBytes) {
|
||||||
const key = await window.crypto.subtle.importKey(
|
const encrypted = await window.crypto.subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, dataBytes);
|
||||||
'raw',
|
return new Uint8Array(encrypted);
|
||||||
keyBytes,
|
}
|
||||||
{ name: 'HMAC', hash: { name: 'SHA-256' } },
|
|
||||||
false,
|
async function importAesKey(keyBytes) {
|
||||||
['sign']
|
return window.crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']);
|
||||||
);
|
}
|
||||||
const signature = await window.crypto.subtle.sign('HMAC', key, messageBytes);
|
|
||||||
return new Uint8Array(signature);
|
async function aesGcmEncrypt(aesKey, ivBytes, dataBytes) {
|
||||||
|
const ct = await window.crypto.subtle.encrypt({ name: 'AES-GCM', iv: ivBytes }, aesKey, dataBytes);
|
||||||
|
return new Uint8Array(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||||
@@ -68,53 +43,41 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
|||||||
|
|
||||||
const username = document.getElementById('username').value.trim();
|
const username = document.getElementById('username').value.trim();
|
||||||
const password = document.getElementById('password').value;
|
const password = document.getElementById('password').value;
|
||||||
if (!username || !password) {
|
if (!username || !password) { errorEl.textContent = '请输入账户与密码'; return; }
|
||||||
errorEl.textContent = '请输入账户与密码';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const btn = document.getElementById('loginBtn');
|
const btn = document.getElementById('loginBtn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 1: get challenge (nonce + salt)
|
|
||||||
const csrftoken = getCookie('csrftoken');
|
const csrftoken = getCookie('csrftoken');
|
||||||
const chalResp = await fetch('/accounts/challenge/', {
|
const pkResp = await fetch('/accounts/pubkey/', { method: 'GET', credentials: 'same-origin', headers: { 'X-CSRFToken': csrftoken || '' } });
|
||||||
method: 'POST',
|
if (!pkResp.ok) throw new Error('获取公钥失败');
|
||||||
credentials: 'same-origin',
|
const pkJson = await pkResp.json();
|
||||||
headers: {
|
const spkiBytes = new Uint8Array(base64ToArrayBuffer(pkJson.public_key_spki));
|
||||||
'Content-Type': 'application/json',
|
const pubKey = await importRsaPublicKey(spkiBytes);
|
||||||
'X-CSRFToken': csrftoken || ''
|
|
||||||
},
|
const aesKeyRaw = new Uint8Array(32); window.crypto.getRandomValues(aesKeyRaw);
|
||||||
body: JSON.stringify({ username })
|
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 })
|
||||||
});
|
});
|
||||||
if (!chalResp.ok) {
|
const setKeyJson = await setKeyResp.json();
|
||||||
throw new Error('获取挑战失败');
|
if (!setKeyResp.ok || !setKeyJson.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 aesKey = await importAesKey(aesKeyRaw);
|
||||||
const derived = await deriveKey(password, saltBytes, 100000, 32);
|
const iv = new Uint8Array(12); window.crypto.getRandomValues(iv);
|
||||||
const hmac = await hmacSha256(derived, nonceBytes);
|
const payload = new TextEncoder().encode(JSON.stringify({ username, password }));
|
||||||
const hmacB64 = arrayBufferToBase64(hmac);
|
const ct = await aesGcmEncrypt(aesKey, iv, payload);
|
||||||
|
const ctB64 = arrayBufferToBase64(ct);
|
||||||
|
const ivB64 = arrayBufferToBase64(iv);
|
||||||
|
|
||||||
// Step 3: submit login with username and hmac
|
const submitResp = await fetch('/accounts/login/secure-submit/', {
|
||||||
const submitResp = await fetch('/accounts/login/submit/', {
|
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({ username, hmac: hmacB64 })
|
|
||||||
});
|
});
|
||||||
const submitJson = await submitResp.json();
|
const submitJson = await submitResp.json();
|
||||||
if (!submitResp.ok || !submitJson.ok) {
|
if (!submitResp.ok || !submitJson.ok) throw new Error(submitJson.message || '登录失败');
|
||||||
throw new Error(submitJson.message || '登录失败');
|
|
||||||
}
|
|
||||||
// Redirect to home with user_id
|
|
||||||
window.location.href = submitJson.redirect_url;
|
window.location.href = submitJson.redirect_url;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ app_name = "accounts"
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("login/", views.login_page, name="login"),
|
path("login/", views.login_page, name="login"),
|
||||||
path("challenge/", views.challenge, name="challenge"),
|
path("pubkey/", views.pubkey, name="pubkey"),
|
||||||
path("login/submit/", views.login_submit, name="login_submit"),
|
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"),
|
path("logout/", views.logout, name="logout"),
|
||||||
]
|
]
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import hmac
|
|
||||||
|
|
||||||
from django.http import JsonResponse, HttpResponseBadRequest
|
from django.http import JsonResponse, HttpResponseBadRequest
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
@@ -10,7 +9,7 @@ from django.views.decorators.csrf import csrf_protect, ensure_csrf_cookie
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from .es_client import get_user_by_username
|
from .es_client import get_user_by_username
|
||||||
from .crypto import salt_for_username, hmac_sha256
|
from .crypto import get_public_key_spki_b64, rsa_oaep_decrypt_b64, aes_gcm_decrypt_b64, verify_password
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
@@ -19,90 +18,72 @@ def login_page(request):
|
|||||||
return render(request, "accounts/login.html")
|
return render(request, "accounts/login.html")
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["GET"])
|
||||||
@csrf_protect
|
@ensure_csrf_cookie
|
||||||
def challenge(request):
|
def pubkey(request):
|
||||||
try:
|
pk_b64 = get_public_key_spki_b64()
|
||||||
payload = json.loads(request.body.decode("utf-8"))
|
return JsonResponse({"public_key_spki": pk_b64})
|
||||||
except json.JSONDecodeError:
|
|
||||||
return HttpResponseBadRequest("Invalid JSON")
|
|
||||||
|
|
||||||
username = payload.get("username", "").strip()
|
|
||||||
if not username:
|
|
||||||
return HttpResponseBadRequest("Username required")
|
|
||||||
|
|
||||||
# Generate nonce and compute per-username salt
|
|
||||||
nonce = os.urandom(16)
|
|
||||||
salt = salt_for_username(username)
|
|
||||||
|
|
||||||
# Persist challenge in session to prevent replay with mismatched user
|
|
||||||
request.session["challenge_nonce"] = base64.b64encode(nonce).decode("ascii")
|
|
||||||
request.session["challenge_username"] = username
|
|
||||||
|
|
||||||
return JsonResponse({
|
|
||||||
"nonce": base64.b64encode(nonce).decode("ascii"),
|
|
||||||
"salt": base64.b64encode(salt).decode("ascii"),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
@csrf_protect
|
@csrf_protect
|
||||||
def login_submit(request):
|
def set_session_key(request):
|
||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8"))
|
payload = json.loads(request.body.decode("utf-8"))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return HttpResponseBadRequest("Invalid JSON")
|
return HttpResponseBadRequest("Invalid JSON")
|
||||||
|
enc_key_b64 = payload.get("encrypted_key", "")
|
||||||
username = payload.get("username", "").strip()
|
if not enc_key_b64:
|
||||||
client_hmac_b64 = payload.get("hmac", "")
|
|
||||||
if not username or not client_hmac_b64:
|
|
||||||
return HttpResponseBadRequest("Missing fields")
|
return HttpResponseBadRequest("Missing fields")
|
||||||
|
try:
|
||||||
|
key_bytes = rsa_oaep_decrypt_b64(enc_key_b64)
|
||||||
|
except Exception:
|
||||||
|
return HttpResponseBadRequest("Decrypt error")
|
||||||
|
request.session["session_enc_key_b64"] = base64.b64encode(key_bytes).decode("ascii")
|
||||||
|
return JsonResponse({"ok": True})
|
||||||
|
|
||||||
# Validate challenge stored in session
|
@require_http_methods(["POST"])
|
||||||
session_username = request.session.get("challenge_username")
|
@csrf_protect
|
||||||
nonce_b64 = request.session.get("challenge_nonce")
|
def secure_login_submit(request):
|
||||||
if not session_username or not nonce_b64 or session_username != username:
|
try:
|
||||||
return HttpResponseBadRequest("Challenge not found or mismatched user")
|
payload = json.loads(request.body.decode("utf-8"))
|
||||||
|
except json.JSONDecodeError:
|
||||||
# Lookup user in ES (placeholder)
|
return HttpResponseBadRequest("Invalid JSON")
|
||||||
|
iv_b64 = payload.get("iv", "")
|
||||||
|
ct_b64 = payload.get("ciphertext", "")
|
||||||
|
if not iv_b64 or not ct_b64:
|
||||||
|
return HttpResponseBadRequest("Missing fields")
|
||||||
|
key_b64 = request.session.get("session_enc_key_b64")
|
||||||
|
if not key_b64:
|
||||||
|
return HttpResponseBadRequest("Session key missing")
|
||||||
|
try:
|
||||||
|
key_bytes = base64.b64decode(key_b64)
|
||||||
|
pt = aes_gcm_decrypt_b64(key_bytes, iv_b64, ct_b64)
|
||||||
|
obj = json.loads(pt.decode("utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return HttpResponseBadRequest("Decrypt error")
|
||||||
|
username = (obj.get("username") or "").strip()
|
||||||
|
password = (obj.get("password") or "")
|
||||||
|
if not username or not password:
|
||||||
|
return HttpResponseBadRequest("Missing credentials")
|
||||||
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)
|
return JsonResponse({"ok": False, "message": "User not found"}, status=401)
|
||||||
|
if not verify_password(password, user.get("password_salt") or "", user.get("password_hash") or ""):
|
||||||
# Server-side HMAC verification
|
|
||||||
try:
|
|
||||||
nonce = base64.b64decode(nonce_b64)
|
|
||||||
stored_derived_b64 = user.get("password", "")
|
|
||||||
stored_derived = base64.b64decode(stored_derived_b64)
|
|
||||||
server_hmac_b64 = base64.b64encode(hmac_sha256(stored_derived, nonce)).decode("ascii")
|
|
||||||
except Exception:
|
|
||||||
return HttpResponseBadRequest("Verification error")
|
|
||||||
|
|
||||||
if not hmac.compare_digest(server_hmac_b64, client_hmac_b64):
|
|
||||||
return JsonResponse({"ok": False, "message": "Invalid credentials"}, status=401)
|
return JsonResponse({"ok": False, "message": "Invalid credentials"}, status=401)
|
||||||
|
|
||||||
# Successful login: rotate session key and set user session
|
|
||||||
try:
|
try:
|
||||||
request.session.cycle_key()
|
request.session.cycle_key()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
request.session["user_id"] = user["user_id"]
|
request.session["user_id"] = user["user_id"]
|
||||||
request.session["username"] = user["username"]
|
request.session["username"] = user["username"]
|
||||||
try:
|
try:
|
||||||
request.session["permission"] = int(user["permission"]) if user.get("permission") is not None else 1
|
request.session["permission"] = int(user["permission"]) if user.get("permission") is not None else 1
|
||||||
except Exception:
|
except Exception:
|
||||||
request.session["permission"] = 1
|
request.session["permission"] = 1
|
||||||
|
if "session_enc_key_b64" in request.session:
|
||||||
# Clear challenge to prevent reuse
|
del request.session["session_enc_key_b64"]
|
||||||
for k in ("challenge_username", "challenge_nonce"):
|
return JsonResponse({"ok": True, "redirect_url": f"/main/home/?user_id={user['user_id']}"})
|
||||||
if k in request.session:
|
|
||||||
del request.session[k]
|
|
||||||
|
|
||||||
return JsonResponse({
|
|
||||||
"ok": True,
|
|
||||||
"redirect_url": f"/main/home/?user_id={user['user_id']}",
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ class UserDocument(Document):
|
|||||||
"""用户数据文档映射"""
|
"""用户数据文档映射"""
|
||||||
user_id = fields.LongField()
|
user_id = fields.LongField()
|
||||||
username = fields.KeywordField()
|
username = fields.KeywordField()
|
||||||
password = fields.KeywordField() # 还是2种权限,0为管理员,1为用户(区别在于0有全部权限,1在数据管理页面有搜索框,但是索引到的录入信息要根据其用户id查询其key,若其中之一与用户的manage_key字段匹配就显示否则不显示)
|
password_hash = fields.KeywordField()
|
||||||
permission = fields.IntegerField()
|
password_salt = fields.KeywordField()
|
||||||
|
permission = fields.IntegerField() # 还是2种权限,0为管理员,1为用户(区别在于0有全部权限,1在数据管理页面有搜索框,但是索引到的录入信息要根据其用户id查询其key,若其中之一与用户的manage_key字段匹配就显示否则不显示)
|
||||||
key = fields.IntegerField() #表示该用户的关键字,举个例子:学生A的key为"2024届人工智能1班","2024届","计算机与人工智能学院" 班导师B的key为"计算机与人工智能学院"
|
key = fields.IntegerField() #表示该用户的关键字,举个例子:学生A的key为"2024届人工智能1班","2024届","计算机与人工智能学院" 班导师B的key为"计算机与人工智能学院"
|
||||||
manage_key = fields.IntegerField() #表示该用户管理的关键字(非管理员)班导师B的manage_key为"2024届人工智能1班"
|
manage_key = fields.IntegerField() #表示该用户管理的关键字(非管理员)班导师B的manage_key为"2024届人工智能1班"
|
||||||
#那么学生A就可以在数据管理页面搜索到自己的获奖数据,而班导师B就可以在数据管理页面搜索到所有人工智能1班的获奖数据。也就是说学生A和班导师B都其实只有用户权限
|
#那么学生A就可以在数据管理页面搜索到自己的获奖数据,而班导师B就可以在数据管理页面搜索到所有人工智能1班的获奖数据。也就是说学生A和班导师B都其实只有用户权限
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from elasticsearch import Elasticsearch
|
|||||||
from elasticsearch_dsl import connections
|
from elasticsearch_dsl import connections
|
||||||
import os
|
import os
|
||||||
from .documents import AchievementDocument, UserDocument, GlobalDocument
|
from .documents import AchievementDocument, UserDocument, GlobalDocument
|
||||||
|
from accounts.crypto import hash_password_random_salt
|
||||||
from .indexes import ACHIEVEMENT_INDEX_NAME, USER_INDEX_NAME, GLOBAL_INDEX_NAME
|
from .indexes import ACHIEVEMENT_INDEX_NAME, USER_INDEX_NAME, GLOBAL_INDEX_NAME
|
||||||
import hashlib
|
import hashlib
|
||||||
import time
|
import time
|
||||||
@@ -63,10 +64,12 @@ def create_index_with_mapping():
|
|||||||
|
|
||||||
# --- 4. 创建默认管理员用户(可选:也可检查用户是否已存在)---
|
# --- 4. 创建默认管理员用户(可选:也可检查用户是否已存在)---
|
||||||
# 这里简单处理:每次初始化都写入(可能重复),建议加唯一性判断
|
# 这里简单处理:每次初始化都写入(可能重复),建议加唯一性判断
|
||||||
|
_salt_b64, _hash_b64 = hash_password_random_salt("admin")
|
||||||
admin_user = {
|
admin_user = {
|
||||||
"user_id": 0,
|
"user_id": 0,
|
||||||
"username": "admin",
|
"username": "admin",
|
||||||
"password": "admin", # ⚠️ 生产环境务必加密!
|
"password_hash": _hash_b64,
|
||||||
|
"password_salt": _salt_b64,
|
||||||
"permission": 0
|
"permission": 0
|
||||||
}
|
}
|
||||||
# 可选:检查 admin 是否已存在(根据 user_id 或 username)
|
# 可选:检查 admin 是否已存在(根据 user_id 或 username)
|
||||||
@@ -513,10 +516,17 @@ def write_user_data(user_data):
|
|||||||
perm_val = int(user_data.get('permission', 1))
|
perm_val = int(user_data.get('permission', 1))
|
||||||
except Exception:
|
except Exception:
|
||||||
perm_val = 1
|
perm_val = 1
|
||||||
|
pwd = str(user_data.get('password') or '').strip()
|
||||||
|
pwd_hash_b64 = user_data.get('password_hash')
|
||||||
|
pwd_salt_b64 = user_data.get('password_salt')
|
||||||
|
if pwd:
|
||||||
|
salt_b64, hash_b64 = hash_password_random_salt(pwd)
|
||||||
|
pwd_hash_b64, pwd_salt_b64 = hash_b64, salt_b64
|
||||||
user = UserDocument(
|
user = UserDocument(
|
||||||
user_id=user_data.get('user_id'),
|
user_id=user_data.get('user_id'),
|
||||||
username=user_data.get('username'),
|
username=user_data.get('username'),
|
||||||
password=user_data.get('password'),
|
password_hash=pwd_hash_b64,
|
||||||
|
password_salt=pwd_salt_b64,
|
||||||
permission=perm_val
|
permission=perm_val
|
||||||
)
|
)
|
||||||
user.save()
|
user.save()
|
||||||
@@ -535,11 +545,10 @@ def get_user_by_id(user_id):
|
|||||||
if response.hits:
|
if response.hits:
|
||||||
hit = response.hits[0]
|
hit = response.hits[0]
|
||||||
return {
|
return {
|
||||||
"user_id": hit.user_id,
|
"user_id": hit.user_id,
|
||||||
"username": hit.username,
|
"username": hit.username,
|
||||||
"password": hit.password,
|
"permission": hit.permission
|
||||||
"permission": hit.permission
|
}
|
||||||
}
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -566,7 +575,8 @@ def get_user_by_username(username):
|
|||||||
return {
|
return {
|
||||||
"user_id": hit.user_id,
|
"user_id": hit.user_id,
|
||||||
"username": hit.username,
|
"username": hit.username,
|
||||||
"password": hit.password,
|
"password_hash": getattr(hit, 'password_hash', None),
|
||||||
|
"password_salt": getattr(hit, 'password_salt', None),
|
||||||
"permission": int(hit.permission)
|
"permission": int(hit.permission)
|
||||||
}
|
}
|
||||||
return None
|
return None
|
||||||
@@ -639,7 +649,9 @@ def update_user_by_id(user_id, username=None, permission=None, password=None):
|
|||||||
if permission is not None:
|
if permission is not None:
|
||||||
doc.permission = int(permission)
|
doc.permission = int(permission)
|
||||||
if password is not None:
|
if password is not None:
|
||||||
doc.password = password
|
salt_b64, hash_b64 = hash_password_random_salt(str(password))
|
||||||
|
doc.password_hash = hash_b64
|
||||||
|
doc.password_salt = salt_b64
|
||||||
doc.save()
|
doc.save()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
INDEX_NAME = "wordsearch2666661"
|
INDEX_NAME = "wordsearch2666661"
|
||||||
USER_NAME = "users11111"
|
USER_NAME = "users1111166"
|
||||||
ACHIEVEMENT_INDEX_NAME = INDEX_NAME
|
ACHIEVEMENT_INDEX_NAME = INDEX_NAME
|
||||||
USER_INDEX_NAME = USER_NAME
|
USER_INDEX_NAME = USER_NAME
|
||||||
GLOBAL_INDEX_NAME = "global11111111211"
|
GLOBAL_INDEX_NAME = "global1111111121"
|
||||||
|
|||||||
Reference in New Issue
Block a user