diff --git a/Server/src/auth.rs b/Server/src/auth.rs index 69350d6..45e28ef 100644 --- a/Server/src/auth.rs +++ b/Server/src/auth.rs @@ -4,15 +4,14 @@ //! 机器内存紧张,宁可每次打一次很轻的索引查询,也不常驻一份副本: //! - 登录:表 `ccuser(username, password_sha256)`; //! - 入口门:表 `dev_gate(gate_hash)`。 -//! 连接串由环境变量 `DATABASE_URL` 指定,`UserStore` 与 `DevGate` **共用同一个 -//! 连接池**(见 `db_pool_from_env`),避免开两份连接。未设置 `DATABASE_URL` 时各自 -//! 回退到内置/文件兜底,方便本地无 DB 时开发。 +//! 连接串由环境变量 `DATABASE_URL` 指定,`UserStore`/`WallUserStore`/`DevGate` **共用 +//! 同一个连接池**(见 `db_pool_from_env`),避免开多份连接。**强制要求 `DATABASE_URL`**: +//! 未设置则启动即 panic——不再有任何写死/文件兜底,登录与入口门一律走数据库。 //! //! 口令永不明文存储:库里存的是 SHA256(hex),登录时把传入明文同样 //! 哈希后做**等长比较**。(注意:SHA256 仅用于“不落明文”,并非抗暴力破解的 //! 口令哈希;若将来要对外网开放,应换 Argon2/bcrypt 这类慢哈希。) -use std::collections::{HashMap, HashSet}; use std::time::{SystemTime, UNIX_EPOCH}; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; @@ -21,18 +20,19 @@ use sha2::{Digest, Sha256}; use sqlx::mysql::{MySqlPool, MySqlPoolOptions}; /// 按 `DATABASE_URL` 建一个**懒连接**池(启动不阻塞,首次查询才真正建连), -/// 供 `UserStore` 与 `DevGate` 共用。未设置该环境变量则返回 `None`,调用方各自兜底。 -pub fn db_pool_from_env() -> Option { - match std::env::var("DATABASE_URL") { - Ok(url) if !url.trim().is_empty() => { - let pool = MySqlPoolOptions::new() - .max_connections(5) - .connect_lazy(&url) - .expect("DATABASE_URL 格式非法"); - Some(pool) - } - _ => None, - } +/// 供 `UserStore`/`WallUserStore`/`DevGate` 共用。 +/// +/// **强制要求 `DATABASE_URL`**:未设置则直接 panic 拒绝启动——不再有任何写死/文件兜底, +/// 所有登录与入口门一律走数据库。 +pub fn db_pool_from_env() -> MySqlPool { + let url = std::env::var("DATABASE_URL") + .ok() + .filter(|u| !u.trim().is_empty()) + .expect("未设置 DATABASE_URL:登录与入口门已无兜底,必须配置数据库连接串后再启动"); + MySqlPoolOptions::new() + .max_connections(5) + .connect_lazy(&url) + .expect("DATABASE_URL 格式非法") } /// 开发者「入口门」:一组合法的入口 hash。 @@ -43,119 +43,46 @@ pub fn db_pool_from_env() -> Option { /// /// 注意:入口 hash 只是“藏门”的共享口令,**不是身份凭证**。真正证明身份仍靠 /// 登录(用户名 + 密码,见 `UserStore`)。 -pub enum DevGate { +pub struct DevGate { /// 每次校验查 MySQL `dev_gate` 表(不缓存整表)。 - Db(MySqlPool), - /// 内置/文件载入的 hash 集合,仅作无 DB 时的兜底。 - Memory(HashSet), + pool: MySqlPool, } impl DevGate { - /// 有连接池就走 MySQL;否则读 `GATE_FILE`(默认 `dev_gate.txt`),文件也没有再 `seed()`。 - pub fn new(pool: Option) -> Self { - match pool { - Some(p) => { - eprintln!("[gate] 入口校验走 MySQL"); - DevGate::Db(p) - } - None => Self::load_file(), - } + /// 入口门一律走 MySQL(`dev_gate` 表),无任何文件/写死兜底。 + pub fn new(pool: MySqlPool) -> Self { + eprintln!("[gate] 入口校验走 MySQL"); + DevGate { pool } } - /// 从 `GATE_FILE`(默认 `dev_gate.txt`)读入口 hash;读不到回退内置 `seed()`。 - /// 文件格式:一行一个 hash,`#` 之后为注释,空行忽略。 - fn load_file() -> Self { - let path = std::env::var("GATE_FILE").unwrap_or_else(|_| "dev_gate.txt".to_string()); - match std::fs::read_to_string(&path) { - Ok(text) => { - let hashes = Self::parse(&text); - eprintln!("[gate] 从 {path} 载入 {} 个入口 hash", hashes.len()); - DevGate::Memory(hashes) - } - Err(_) => { - eprintln!("[gate] 未找到 {path},回退到内置写死列表"); - Self::seed() - } - } - } - - /// 把文件文本解析为 hash 集合:丢掉 `#` 注释与空行,每行取剩下的非空串。 - fn parse(text: &str) -> HashSet { - text.lines() - .map(|line| line.split('#').next().unwrap_or("").trim()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) - .collect() - } - - /// 内置写死的入口 hash 表,作为无 DB / 无文件时的兜底。 - pub fn seed() -> Self { - let mut hashes = HashSet::new(); - - // cc:SHA256("192118Lht") 的前 12 位。对应入口 域名/#0086e83c9b10 - hashes.insert("0086e83c9b10".to_string()); - - DevGate::Memory(hashes) - } - - /// 该 hash 是否属于某个合法开发者。DB 模式下每次查库(命中即放行); + /// 该 hash 是否属于某个合法开发者。每次查库(命中即放行); /// 查询出错按“不放行”处理,不泄露后端内部状态。 pub async fn allows(&self, hash: &str) -> bool { - match self { - DevGate::Memory(hashes) => hashes.contains(hash), - DevGate::Db(pool) => { - let row: Result, sqlx::Error> = - sqlx::query_as("SELECT gate_hash FROM dev_gate WHERE gate_hash = ?") - .bind(hash) - .fetch_optional(pool) - .await; - match row { - Ok(found) => found.is_some(), - Err(e) => { - eprintln!("[gate] 查询 dev_gate 失败: {e}"); - false - } - } + let row: Result, sqlx::Error> = + sqlx::query_as("SELECT gate_hash FROM dev_gate WHERE gate_hash = ?") + .bind(hash) + .fetch_optional(&self.pool) + .await; + match row { + Ok(found) => found.is_some(), + Err(e) => { + eprintln!("[gate] 查询 dev_gate 失败: {e}"); + false } } } } -/// 用户校验后端:优先连 MySQL,无 `DATABASE_URL` 时退回内置写死表。 -pub enum UserStore { - /// 从 MySQL 的 `ccuser` 表查 `password_sha256`。 - Db(MySqlPool), - /// 内置写死表(用户名 → 密码SHA256 hex),仅作无 DB 时的兜底。 - Memory(HashMap), +/// 用户校验后端:一律走 MySQL `ccuser` 表,无写死兜底。 +pub struct UserStore { + pool: MySqlPool, } impl UserStore { - /// 有连接池就走 MySQL(每次校验查 `ccuser`);否则回退到内置 `seed()`。 - pub fn new(pool: Option) -> Self { - match pool { - Some(p) => { - eprintln!("[auth] 用户校验走 MySQL"); - UserStore::Db(p) - } - None => { - eprintln!("[auth] 未设置 DATABASE_URL,回退到内置写死用户表"); - Self::seed() - } - } - } - - /// 内置写死的单用户表,作为 `load()` 无 DB 配置时的兜底。 - pub fn seed() -> Self { - let mut users = HashMap::new(); - - // cc / 192118Lht - // 下面这串是 SHA256("192118Lht") 的 hex,明文不进源码。 - users.insert( - "cc".to_string(), - "0086e83c9b108b227eed55425e9641286f42bd0c31a1b95afbb9edd6d3aa6234".to_string(), - ); - - UserStore::Memory(users) + /// 每次校验查 `ccuser`,无任何内置兜底。 + pub fn new(pool: MySqlPool) -> Self { + eprintln!("[auth] 用户校验走 MySQL"); + UserStore { pool } } /// 校验用户名 + 明文密码是否匹配。 @@ -164,21 +91,16 @@ impl UserStore { /// 不向外区分(避免泄露“某用户名是否存在”或后端内部状态)。 pub async fn verify(&self, username: &str, password: &str) -> bool { let actual_hex = sha256_hex(password); - let expected_hex = match self { - UserStore::Memory(users) => users.get(username).cloned(), - UserStore::Db(pool) => { - let row: Result, sqlx::Error> = - sqlx::query_as("SELECT password_sha256 FROM ccuser WHERE username = ?") - .bind(username) - .fetch_optional(pool) - .await; - match row { - Ok(found) => found.map(|(hex,)| hex), - Err(e) => { - eprintln!("[auth] 查询 ccuser 失败: {e}"); - None - } - } + let row: Result, sqlx::Error> = + sqlx::query_as("SELECT password_sha256 FROM ccuser WHERE username = ?") + .bind(username) + .fetch_optional(&self.pool) + .await; + let expected_hex = match row { + Ok(found) => found.map(|(hex,)| hex), + Err(e) => { + eprintln!("[auth] 查询 ccuser 失败: {e}"); + None } }; match expected_hex { @@ -188,65 +110,29 @@ impl UserStore { } } -/// 照片墙用户校验后端:查 `wall_users` 表(与 `ccuser` 物理隔离)。 -/// 未设 DATABASE_URL 时退回内置兜底。 -pub enum WallUserStore { - Db(MySqlPool), - Memory(HashMap), +/// 照片墙用户校验后端:一律走 MySQL `wall_users` 表(与 `ccuser` 物理隔离),无写死兜底。 +pub struct WallUserStore { + pool: MySqlPool, } impl WallUserStore { - pub fn new(pool: Option) -> Self { - match pool { - Some(p) => { - eprintln!("[auth] 照片墙用户校验走 MySQL (wall_users)"); - WallUserStore::Db(p) - } - None => { - eprintln!("[auth] 未设置 DATABASE_URL,照片墙用户回退到内置表"); - Self::seed() - } - } - } - - /// 内置兜底:cc + UserTest - pub fn seed() -> Self { - let mut users = HashMap::new(); - // cc / 192118Lht - users.insert( - "cc".to_string(), - "0086e83c9b108b227eed55425e9641286f42bd0c31a1b95afbb9edd6d3aa6234".to_string(), - ); - // UserTest / 2015.08.607 - users.insert( - "UserTest".to_string(), - "76f03f2eaa05e66c82d25e168fec27e7d0d202f15da73aa5852562f863633b8e".to_string(), - ); - // UserTest6340 / 2019.01.6340 - users.insert( - "UserTest6340".to_string(), - "c7780d075f561283ae4665a64d8b8e2642389ad948390a542b847833a7ce7c1a".to_string(), - ); - WallUserStore::Memory(users) + pub fn new(pool: MySqlPool) -> Self { + eprintln!("[auth] 照片墙用户校验走 MySQL (wall_users)"); + WallUserStore { pool } } pub async fn verify(&self, username: &str, password: &str) -> bool { let actual_hex = sha256_hex(password); - let expected_hex = match self { - WallUserStore::Memory(users) => users.get(username).cloned(), - WallUserStore::Db(pool) => { - let row: Result, sqlx::Error> = - sqlx::query_as("SELECT password_sha256 FROM wall_users WHERE username = ?") - .bind(username) - .fetch_optional(pool) - .await; - match row { - Ok(found) => found.map(|(hex,)| hex), - Err(e) => { - eprintln!("[auth] 查询 wall_users 失败: {e}"); - None - } - } + let row: Result, sqlx::Error> = + sqlx::query_as("SELECT password_sha256 FROM wall_users WHERE username = ?") + .bind(username) + .fetch_optional(&self.pool) + .await; + let expected_hex = match row { + Ok(found) => found.map(|(hex,)| hex), + Err(e) => { + eprintln!("[auth] 查询 wall_users 失败: {e}"); + None } }; match expected_hex { diff --git a/Server/src/state.rs b/Server/src/state.rs index bf94577..3d3aeb9 100644 --- a/Server/src/state.rs +++ b/Server/src/state.rs @@ -17,7 +17,7 @@ use crate::weather::WeatherService; pub struct AppState { /// 最新一次采样快照。 pub snapshot: Arc>, - /// 用户校验后端(MySQL 或内置兜底):内部已是连接池/只读 map,`Arc` 共享即可。 + /// 用户校验后端(MySQL `ccuser`):内部持连接池,`Arc` 共享即可。 pub users: Arc, /// 开发者入口门:合法入口 hash 集合,只读共享。 pub gate: Arc, @@ -36,6 +36,7 @@ pub struct AppState { impl AppState { pub fn new() -> Self { // 一个连接池给登录校验与入口门共用(克隆是廉价的、共享底层连接)。 + // db_pool_from_env 强制要求 DATABASE_URL,缺失则在此 panic 拒绝启动。 let pool = auth::db_pool_from_env(); Self { snapshot: Arc::new(RwLock::new(Stats::default())),