feat(auth): issue/verify username-bearing HS256 JWT
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
42639856b2
commit
e1820d0ec8
@ -13,9 +13,10 @@
|
||||
//! 口令哈希;若将来要对外网开放,应换 Argon2/bcrypt 这类慢哈希。)
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::mysql::{MySqlPool, MySqlPoolOptions};
|
||||
|
||||
@ -187,19 +188,65 @@ impl UserStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// 签发一个登录 token。
|
||||
///
|
||||
/// 不引入 `rand`/`uuid`:用「纳秒时间戳 + 进程内单调计数器」拼出唯一输入,
|
||||
/// 再 SHA256 成不可预测的 hex。够当登录态标识用;将来要做真正的会话校验,
|
||||
/// 应换成签名 token(如 JWT)或服务端会话表。
|
||||
pub fn issue_token() -> String {
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
let nanos = SystemTime::now()
|
||||
/// JWT 载荷:`sub` 为用户名,`exp` 为到期 Unix 秒。
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Claims {
|
||||
sub: String,
|
||||
exp: usize,
|
||||
}
|
||||
|
||||
/// 签发登录 token:带用户名的 HS256 JWT,30 天过期。密钥取 `JWT_SECRET`。
|
||||
pub fn issue_token(username: &str) -> String {
|
||||
let exp = now_secs() + 30 * 24 * 3600;
|
||||
sign(&jwt_secret(), username, exp)
|
||||
}
|
||||
|
||||
/// 校验 token,成功返回其中的用户名。过期/签名不符/格式错均返回 `None`。
|
||||
pub fn verify_token(token: &str) -> Option<String> {
|
||||
verify(&jwt_secret(), token)
|
||||
}
|
||||
|
||||
/// 用指定密钥签一个 JWT(便于测试,不读环境变量)。
|
||||
fn sign(secret: &[u8], username: &str, exp: usize) -> String {
|
||||
let claims = Claims {
|
||||
sub: username.to_string(),
|
||||
exp,
|
||||
};
|
||||
encode(
|
||||
&Header::default(),
|
||||
&claims,
|
||||
&EncodingKey::from_secret(secret),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 用指定密钥校验 JWT(便于测试)。默认校验 `exp`。
|
||||
fn verify(secret: &[u8], token: &str) -> Option<String> {
|
||||
decode::<Claims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(secret),
|
||||
&Validation::default(),
|
||||
)
|
||||
.ok()
|
||||
.map(|data| data.claims.sub)
|
||||
}
|
||||
|
||||
/// 从 `JWT_SECRET` 取签名密钥;未设置则用内置开发密钥并告警(生产务必设置)。
|
||||
fn jwt_secret() -> Vec<u8> {
|
||||
match std::env::var("JWT_SECRET") {
|
||||
Ok(s) if !s.trim().is_empty() => s.into_bytes(),
|
||||
_ => {
|
||||
eprintln!("[auth] 警告:未设置 JWT_SECRET,使用内置开发密钥(生产环境务必设置)");
|
||||
b"dev-insecure-secret-change-me".to_vec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn now_secs() -> usize {
|
||||
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}"))
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0) as usize
|
||||
}
|
||||
|
||||
/// 计算 SHA256 并返回小写 hex 字符串。
|
||||
@ -224,3 +271,40 @@ fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
|
||||
}
|
||||
diff == 0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn jwt_round_trips_username() {
|
||||
let secret = b"test-secret";
|
||||
let token = sign(secret, "cc", far_future());
|
||||
assert_eq!(verify(secret, &token), Some("cc".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jwt_rejects_wrong_secret() {
|
||||
let token = sign(b"secret-a", "cc", far_future());
|
||||
assert_eq!(verify(b"secret-b", &token), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jwt_rejects_garbage() {
|
||||
assert_eq!(verify(b"s", "not-a-jwt"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jwt_rejects_expired() {
|
||||
let token = sign(b"s", "cc", 1); // 1970 年过期
|
||||
assert_eq!(verify(b"s", &token), None);
|
||||
}
|
||||
|
||||
fn far_future() -> usize {
|
||||
(std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
+ 3600) as usize
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,8 +98,8 @@ async fn login(
|
||||
if state.users.verify(&req.username, &req.password).await {
|
||||
Ok(Json(LoginResponse {
|
||||
ok: true,
|
||||
username: req.username,
|
||||
token: crate::auth::issue_token(),
|
||||
username: req.username.clone(),
|
||||
token: crate::auth::issue_token(&req.username),
|
||||
}))
|
||||
} else {
|
||||
Err(StatusCode::UNAUTHORIZED)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user