"""登录: 标注人账号在环境变量 ANNOTATORS_JSON 里配置(密码只存 PBKDF2 哈希), 会话用 HMAC 签名 cookie。 ANNOTATORS_JSON 示例(scripts/hash_password.py 生成哈希): { "Rong": {"role": "admin", "pw": "pbkdf2_sha256$..."}, "A": {"pw": "pbkdf2_sha256$...", "papers": ["MICCAI_3979_paper"]}, "B": {"pw": "pbkdf2_sha256$..."} } papers 可选: 给了就只能看到这些论文。repo 可选: 给了就把此人标注写进那个仓库(默认写 DATA_REPO 的 annotations//)。未配置 ANNOTATORS_JSON 且为 local 后端时 = 开放模式(同 0817 本地版, 任填 ID)。 """ import base64 import hashlib import hmac import json import secrets import time from typing import Optional ITERATIONS = 200_000 COOKIE = "repl_sess" SESSION_TTL = 14 * 24 * 3600 def hash_password(pw: str, salt: Optional[str] = None) -> str: salt = salt or secrets.token_hex(16) dk = hashlib.pbkdf2_hmac("sha256", pw.encode(), salt.encode(), ITERATIONS) return f"pbkdf2_sha256${ITERATIONS}${salt}${dk.hex()}" def verify_password(pw: str, stored: str) -> bool: try: algo, it, salt, digest = stored.split("$") except ValueError: return False if algo != "pbkdf2_sha256": return False dk = hashlib.pbkdf2_hmac("sha256", pw.encode(), salt.encode(), int(it)) return hmac.compare_digest(dk.hex(), digest) def _b64(b: bytes) -> str: return base64.urlsafe_b64encode(b).decode().rstrip("=") def _unb64(s: str) -> bytes: return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)) def sign_session(secret: str, annotator: str) -> str: body = _b64(json.dumps({"a": annotator, "exp": int(time.time()) + SESSION_TTL}).encode()) mac = _b64(hmac.new(secret.encode(), body.encode(), hashlib.sha256).digest()) return f"{body}.{mac}" def read_session(secret: str, token: Optional[str]) -> Optional[str]: if not token or "." not in token: return None body, mac = token.rsplit(".", 1) want = _b64(hmac.new(secret.encode(), body.encode(), hashlib.sha256).digest()) if not hmac.compare_digest(mac, want): return None try: data = json.loads(_unb64(body)) except (ValueError, json.JSONDecodeError): return None if data.get("exp", 0) < time.time(): return None return data.get("a")