//! 用户存储与登录校验。 //! //! 设计取舍:用户数很少(≤ 20),所以**不上数据库**——一个进程内的 //! `HashMap<用户名, 密码SHA256(hex)>` 足矣,启动时构建一次、之后只读。 //! 数据库要付出的连接池、迁移、Schema 维护成本,在这个规模下并不划算。 //! //! 现在用户表是**写死**在 `UserStore::seed()` 里的(仅 `cc` 一个)。 //! 将来用户变多时,迁移路径很短,且 handler 完全无需改动: //! 1. 改成从 TOML/JSON 文件读:把 `seed()` 换成读文件 → 解析 → 填 map; //! 2. 真要运行时增删用户 / 权限 / 审计,再考虑换 SQLite/Postgres。 //! //! 口令永不明文存储:表里存的是 SHA256(hex),登录时把传入明文同样 //! 哈希后做**等长比较**。(注意:SHA256 仅用于“不落明文”,并非抗暴力破解的 //! 口令哈希;若将来要对外网开放,应换 Argon2/bcrypt 这类慢哈希。) use std::collections::HashMap; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; use sha2::{Digest, Sha256}; /// 进程内用户表:用户名 → 密码的 SHA256(小写 hex)。 pub struct UserStore { users: HashMap, } impl UserStore { /// 构建写死的用户表。将来可替换为「从文件读取」。 pub fn seed() -> Self { let mut users = HashMap::new(); // cc / 192118Lht // 下面这串是 SHA256("192118Lht") 的 hex,明文不进源码。 users.insert( "cc".to_string(), "0086e83c9b108b227eed55425e9641286f42bd0c31a1b95afbb9edd6d3aa6234".to_string(), ); Self { users } } /// 校验用户名 + 明文密码是否匹配。 /// /// 用户名不存在与密码错误都返回 `false`,不向外区分两者 /// (避免泄露“某用户名是否存在”)。 pub fn verify(&self, username: &str, password: &str) -> bool { match self.users.get(username) { Some(expected_hex) => { let actual_hex = sha256_hex(password); constant_time_eq(actual_hex.as_bytes(), expected_hex.as_bytes()) } None => false, } } } /// 签发一个登录 token。 /// /// 不引入 `rand`/`uuid`:用「纳秒时间戳 + 进程内单调计数器」拼出唯一输入, /// 再 SHA256 成不可预测的 hex。够当登录态标识用;将来要做真正的会话校验, /// 应换成签名 token(如 JWT)或服务端会话表。 pub fn issue_token() -> String { static COUNTER: AtomicU64 = AtomicU64::new(0); let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_nanos()) .unwrap_or(0); let n = COUNTER.fetch_add(1, Ordering::Relaxed); sha256_hex(&format!("{nanos}-{n}")) } /// 计算 SHA256 并返回小写 hex 字符串。 fn sha256_hex(input: &str) -> String { let digest = Sha256::digest(input.as_bytes()); let mut out = String::with_capacity(digest.len() * 2); for byte in digest { out.push_str(&format!("{byte:02x}")); } out } /// 等长定值时间比较,避免按字节短路造成的时序侧信道。 /// 两串长度不同直接判否(hex 定长 64,正常等长)。 fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { if a.len() != b.len() { return false; } let mut diff = 0u8; for (x, y) in a.iter().zip(b.iter()) { diff |= x ^ y; } diff == 0 }