PersonalWebApplication/Server/src/auth.rs
cc 58284f3fe0 feat: add login/auth system + blog page with rainbow design
- Client: auth API wrapper (api/auth.ts) + useAuth hook with localStorage persistence

- Client: LoginPage/LoginForm with remember-me and loading states

- Client: BlogPage with hero section, rainbow spotlight cursor effect, post list

- Client: CSS rainbow gradient utilities for text/borders/backgrounds

- Server: POST /api/web/login with username/password verification + token

- Server: CORS extended to POST + Content-Type header

- Server: auth.rs token issuer, serde dependency for JSON

- Chore: gitignore Server/.vite

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-18 00:00:38 +08:00

94 lines
3.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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