From 2594ef6dd1eac1d9fff20ba956ee568ea1ea7ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E8=BF=90=E5=AE=B6?= Date: Wed, 22 May 2024 18:00:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0token=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 48 +++++++++++++++++++ Cargo.toml | 3 +- app.toml | 7 ++- library/Cargo.toml | 3 +- library/src/core/config.rs | 8 ++++ library/src/lib.rs | 3 +- library/src/token-backup | 98 ++++++++++++++++++++++++++++++++++++++ library/src/token.rs | 46 ++++++++++++++++++ 8 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 library/src/token-backup create mode 100644 library/src/token.rs diff --git a/Cargo.lock b/Cargo.lock index 298d6ce..6890e55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1015,6 +1015,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1047,6 +1062,7 @@ dependencies = [ "http", "http-body", "http-body-util", + "jsonwebtoken", "moka", "once_cell", "serde", @@ -1211,6 +1227,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1374,6 +1400,16 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.0", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1937,6 +1973,18 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index 7e14580..4ad0a2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,4 +38,5 @@ moka = "0.12" futures-util = "0.3" reqwest = "0.12" futures-executor = "0.3" -error-stack = "0.4" \ No newline at end of file +error-stack = "0.4" +jsonwebtoken = "9.3.0" \ No newline at end of file diff --git a/app.toml b/app.toml index 4937c0a..d57a3d8 100644 --- a/app.toml +++ b/app.toml @@ -10,4 +10,9 @@ level = "DEBUG" [database] url = "postgres://lyj:1325479Lyj!@47.95.198.7:13207/demo_rs" -options = { min_conns = 10, max_conns = 20, conn_timeout = 30, idle_timeout = 300, max_lifetime = 60, sql_logging = true } \ No newline at end of file +options = { min_conns = 10, max_conns = 20, conn_timeout = 30, idle_timeout = 300, max_lifetime = 60, sql_logging = true } + +[jwt] +secret = "chuanyue" +expires = 1800 +refresh_expires = 3600 \ No newline at end of file diff --git a/library/Cargo.toml b/library/Cargo.toml index 432f7cc..ccc798f 100644 --- a/library/Cargo.toml +++ b/library/Cargo.toml @@ -23,4 +23,5 @@ http-body = { workspace = true } http-body-util = { workspace = true } moka = { workspace = true, features = ["future"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros" ] } -futures-util = { workspace = true } \ No newline at end of file +futures-util = { workspace = true } +jsonwebtoken = { workspace = true } \ No newline at end of file diff --git a/library/src/core/config.rs b/library/src/core/config.rs index 0b36a4f..b8408cc 100644 --- a/library/src/core/config.rs +++ b/library/src/core/config.rs @@ -8,6 +8,7 @@ pub struct Config { pub server: Server, pub logger: Logger, pub database: Database, + pub jwt: Jwt } #[derive(Clone, Debug, Deserialize)] @@ -39,6 +40,13 @@ pub struct Logger { pub level: String, } +#[derive(Clone, Debug, Deserialize)] +pub struct Jwt { + pub secret: String, + pub expires: usize, + pub refresh_expires: usize, +} + #[macro_export] macro_rules! config { () => { diff --git a/library/src/lib.rs b/library/src/lib.rs index 5037bb8..5b1a317 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -2,4 +2,5 @@ extern crate self as library; pub mod core; pub mod resp; -pub mod middleware; \ No newline at end of file +pub mod middleware; +pub mod token; \ No newline at end of file diff --git a/library/src/token-backup b/library/src/token-backup new file mode 100644 index 0000000..34fa47e --- /dev/null +++ b/library/src/token-backup @@ -0,0 +1,98 @@ +// claims +use chrono::{Duration, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +struct Claims { + sub: i32, // 用户ID + exp: usize, // Access Token过期时间戳 + r_exp: usize, // Refresh Token过期时间戳 +} + +// 登录API +async fn login(username: String, password: String) -> Result<(String, String), (StatusCode, String)> { + // 模拟数据库查询 + let user = get_user_from_database(&username, &password)?; + // 如果用户存在且密码正确 + let access_token_expiration = Utc::now() + Duration::minutes(15); + let refresh_token_expiration = Utc::now() + Duration::days(30); + + let access_claims = Claims { + sub: user.id, + exp: access_token_expiration.timestamp() as usize, + r_exp: refresh_token_expiration.timestamp() as usize, + }; + let refresh_claims = Claims { + sub: user.id, + exp: refresh_token_expiration.timestamp() as usize, + r_exp: refresh_token_expiration.timestamp() as usize, + }; + + let access_token = jsonwebtoken::encode(&jsonwebtoken::Header::default(), &access_claims, JWT_SECRET)?; + let refresh_token = jsonwebtoken::encode(&jsonwebtoken::Header::default(), &refresh_claims, JWT_SECRET)?; + + Ok((access_token, refresh_token)) +} + +//中间件实现Token校验 +async fn authenticate_access_token(req: Request, next: Next) -> Result { + let auth_header = req.headers().get(header::AUTHORIZATION); + let token = match auth_header { + Some(header_value) => { + let parts: Vec<&str> = header_value.to_str().unwrap_or("").split_whitespace().collect(); + if parts.len() != 2 || parts[0] != "Bearer" { + return Err((StatusCode::BAD_REQUEST, "Invalid authorization header format".to_string())); + } + parts[1] + }, + None => return Err((StatusCode::UNAUTHORIZED, "Missing authorization header".to_string())), + }; + + let validation = Validation::default(); + match decode::(token, &DecodingKey::from_secret(JWT_SECRET), &validation) { + Ok(decoded) => { + // 将Claims附加到请求扩展中,以便后续处理使用 + req.extensions_mut().insert(decoded.claims); + Ok(next.run(req).await) + }, + Err(_) => Err((StatusCode::UNAUTHORIZED, "Invalid token".to_string())), + } +} + +//刷新Token的API +async fn refresh_token_handler(Json(payload): Json) -> Result { + let user_id = payload.sub; + // 在数据库中验证Refresh Token,确保它没有被使用过 + // 这里简化为仅检查用户ID + if let Some(refresh_claims) = get_refresh_claims_from_database(user_id) { + // 生成新的Access Token + let access_token_expiration = Utc::now() + Duration::minutes(15); + let access_claims = Claims { + sub: user_id, + exp: access_token_expiration.timestamp() as usize, + r_exp: refresh_claims.r_exp, + }; + let new_access_token = jsonwebtoken::encode(&jsonwebtoken::Header::default(), &access_claims, JWT_SECRET)?; + Ok(new_access_token) + } else { + Err((StatusCode::UNAUTHORIZED, "Invalid or expired refresh token".to_string())) + } +} + + +// 组合路由和中间件 +use axum::{routing::post, Router}; + +#[tokio::main] +async fn main() { + let app = Router::new() + .route("/login", post(login)) + .route("/refresh-token", post(refresh_token_handler)) + // 示例路由,假设所有路由都需要认证,除了登录和刷新Token + .route("/protected-resource", post(|_| async { /* 处理逻辑 */ })) + .layer(tower::ServiceBuilder::new().layer_fn(authenticate_access_token)); + + println!("Server running on http://localhost:3000"); + axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()).serve(app.into_make_service()).await.unwrap(); +} + diff --git a/library/src/token.rs b/library/src/token.rs new file mode 100644 index 0000000..a4d338d --- /dev/null +++ b/library/src/token.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; + +use crate::config; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claim { + sub: i32, // 用户ID + exp: usize, // Token过期时间戳 +} + +pub fn generate_token(sub: i32) -> String { + let claim = Claim { + sub, + exp: config!().jwt.expires, + }; + generate(claim) +} + +pub fn generate_refresh_token(sub: i32) -> String { + let claim = Claim { + sub, + exp: config!().jwt.refresh_expires, + }; + generate(claim) +} + +fn generate(claim: Claim) -> String { + let token = jsonwebtoken::encode( + &jsonwebtoken::Header::default(), + &claim, + &jsonwebtoken::EncodingKey::from_secret(b"secret"), + ); + token.unwrap_or_else(|e| { + tracing::error!(error =?e, "生成Token失败"); + "".to_string() + }) +} + +pub fn verify_token(token: &str) -> Result { + jsonwebtoken::decode::( + token, + &jsonwebtoken::DecodingKey::from_secret(b"secret"), + &jsonwebtoken::Validation::default(), + ) + .map(|data| data.claims) +} \ No newline at end of file