PersonalWebApplication/Server/src/auth.rs

94 lines
3.5 KiB
Rust
Raw Normal View History

//! 用户存储与登录校验。
//!
//! 设计取舍:用户数很少(≤ 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<String, String>,
}
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
}