From 1bbd77756557117d46bd5f2b3cada39e1118441d Mon Sep 17 00:00:00 2001 From: spdis Date: Mon, 10 Nov 2025 13:38:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=99=BB=E5=BD=95=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E7=AD=89=E5=BE=85=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E8=BF=9B=E4=B8=80=E6=AD=A5=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/crypto.py | 20 ++++++++++++++ accounts/es_client.py | 43 ++++++++++++++++--------------- accounts/static/accounts/login.js | 2 ++ accounts/views.py | 13 +++++----- main/templates/main/home.html | 1 + main/views.py | 2 +- 6 files changed, 52 insertions(+), 29 deletions(-) create mode 100644 accounts/crypto.py diff --git a/accounts/crypto.py b/accounts/crypto.py new file mode 100644 index 0000000..b3d22e7 --- /dev/null +++ b/accounts/crypto.py @@ -0,0 +1,20 @@ +import hashlib +import hmac + + +def salt_for_username(username: str) -> bytes: + """Derive a per-username salt using SHA-256(username). + + The salt is deterministic for a given username and does not require storage. + """ + return hashlib.sha256(username.encode('utf-8')).digest() + + +def derive_password(password_plain: str, salt: bytes, iterations: int = 100_000, dklen: int = 32) -> bytes: + """PBKDF2-SHA256 derive a fixed-length secret from a plaintext password and salt.""" + return hashlib.pbkdf2_hmac('sha256', password_plain.encode('utf-8'), salt, iterations, dklen=dklen) + + +def hmac_sha256(key: bytes, message: bytes) -> bytes: + """Compute HMAC-SHA256 signature for the given message using key bytes.""" + return hmac.new(key, message, hashlib.sha256).digest() \ No newline at end of file diff --git a/accounts/es_client.py b/accounts/es_client.py index 37eba58..930cca7 100644 --- a/accounts/es_client.py +++ b/accounts/es_client.py @@ -1,14 +1,6 @@ import base64 -import hashlib from elastic.es_connect import get_user_by_username as es_get_user_by_username - - -def _salt_for_username(username: str) -> bytes: - return hashlib.sha256(username.encode('utf-8')).digest() - - -def _derive_password(password_plain: str, salt: bytes) -> bytes: - return hashlib.pbkdf2_hmac('sha256', password_plain.encode('utf-8'), salt, 100_000, dklen=32) +from .crypto import salt_for_username, derive_password def get_user_by_username(username: str): @@ -16,18 +8,27 @@ def get_user_by_username(username: str): 从Elasticsearch获取用户数据 """ # 首先尝试从ES获取用户数据 - es_user = es_get_user_by_username(username) - salt = _salt_for_username(username) - derived = _derive_password(es_user.get('password', ''), salt) - if es_user: - # 如果ES中有用户数据,使用ES中的密码 - return { - 'user_id': es_user.get('user_id', 0), - 'username': es_user.get('username', ''), - 'password': base64.b64encode(derived).decode('ascii'), - 'premission': es_user.get('permission', 1), - 'salt': base64.b64encode(salt).decode('ascii'), - } + # es_user = es_get_user_by_username(username) + # if es_user: + # salt = salt_for_username(username) + # derived = derive_password(es_user.get('password', ''), salt) + # # 如果ES中有用户数据,使用ES中的密码 + # return { + # 'user_id': es_user.get('user_id', 0), + # 'username': es_user.get('username', ''), + # 'password': base64.b64encode(derived).decode('ascii'), + # 'permission': es_user.get('permission', 1), + # } + salt = salt_for_username('admin') + derived = derive_password('admin', salt) + + return { + 'user_id': 0, + 'username': 'admin', + 'password': base64.b64encode(derived).decode('ascii'), + 'permission': 0, + } + return None \ No newline at end of file diff --git a/accounts/static/accounts/login.js b/accounts/static/accounts/login.js index 8afb0e4..9de32aa 100644 --- a/accounts/static/accounts/login.js +++ b/accounts/static/accounts/login.js @@ -81,6 +81,7 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => { const csrftoken = getCookie('csrftoken'); const chalResp = await fetch('/accounts/challenge/', { method: 'POST', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' @@ -102,6 +103,7 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => { // 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 || '' diff --git a/accounts/views.py b/accounts/views.py index 3d3d6c6..a1dc9b1 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,6 +1,7 @@ import base64 import json import os +import hmac from django.http import JsonResponse, HttpResponseBadRequest from django.shortcuts import render, redirect @@ -8,7 +9,8 @@ from django.views.decorators.http import require_http_methods from django.views.decorators.csrf import csrf_protect from django.conf import settings -from .es_client import get_user_by_username, _salt_for_username +from .es_client import get_user_by_username +from .crypto import salt_for_username, hmac_sha256 @require_http_methods(["GET"]) @@ -30,7 +32,7 @@ def challenge(request): # Generate nonce and compute per-username salt nonce = os.urandom(16) - salt = _salt_for_username(username) + salt = salt_for_username(username) # Persist challenge in session to prevent replay with mismatched user request.session["challenge_nonce"] = base64.b64encode(nonce).decode("ascii") @@ -71,10 +73,7 @@ def login_submit(request): nonce = base64.b64decode(nonce_b64) stored_derived_b64 = user.get("password", "") stored_derived = base64.b64decode(stored_derived_b64) - # HMAC-SHA256: server computes with stored derived secret - import hmac, hashlib - server_hmac = hmac.new(stored_derived, nonce, hashlib.sha256).digest() - server_hmac_b64 = base64.b64encode(server_hmac).decode("ascii") + server_hmac_b64 = base64.b64encode(hmac_sha256(stored_derived, nonce)).decode("ascii") except Exception: return HttpResponseBadRequest("Verification error") @@ -89,7 +88,7 @@ def login_submit(request): request.session["user_id"] = user["user_id"] request.session["username"] = user["username"] - request.session["premission"] = user["premission"] + request.session["permission"] = user["permission"] # Clear challenge to prevent reuse for k in ("challenge_username", "challenge_nonce"): diff --git a/main/templates/main/home.html b/main/templates/main/home.html index 20ed294..6de7308 100644 --- a/main/templates/main/home.html +++ b/main/templates/main/home.html @@ -38,6 +38,7 @@ try { const resp = await fetch('/accounts/logout/', { method: 'POST', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' diff --git a/main/views.py b/main/views.py index e704199..dae6079 100644 --- a/main/views.py +++ b/main/views.py @@ -6,7 +6,7 @@ from django.views.decorators.http import require_http_methods def home(request): # Enforce login: require session user_id session_user_id = request.session.get("user_id") - if not session_user_id: + if session_user_id is None: return redirect("/accounts/login/") # Show user_id (prefer query param if present, but don't trust it)