From 1c2eb8224841a0c2c5e9eb06b9ec4e5c4c65ece2 Mon Sep 17 00:00:00 2001 From: Milky0217 Date: Fri, 26 Sep 2025 13:48:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E4=BA=86jwt=E4=BD=9C?= =?UTF-8?q?=E4=B8=BA=E8=BA=AB=E4=BB=BD=E9=AA=8C=E8=AF=81=E5=92=8C=E5=88=86?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 50 ++++++++++++++++++ Cargo.toml | 1 + src/auth.rs | 81 +++++++++++++++++++++++++++++ src/main.rs | 141 ++++++++++++++++++++++++++------------------------ src/models.rs | 20 ++++--- 5 files changed, 217 insertions(+), 76 deletions(-) create mode 100644 src/auth.rs diff --git a/Cargo.lock b/Cargo.lock index b602ba0..869633c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -786,8 +786,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1242,6 +1244,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1395,6 +1412,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1536,6 +1563,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1823,6 +1860,7 @@ dependencies = [ "chrono", "dotenvy", "error", + "jsonwebtoken", "reqwest", "serde", "serde_json", @@ -2023,6 +2061,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.11" diff --git a/Cargo.toml b/Cargo.toml index a261e73..bcb174a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ actix-web = "4.11.0" chrono = {version = "0.4.41", features=["serde"]} dotenvy = "0.15.7" error = "0.1.9" +jsonwebtoken = "9.3.1" reqwest = { version = "0.12.23", features=["json"]} serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.143" diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..74fdcb3 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,81 @@ +use chrono::Utc; +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode}; + +use crate::models::Claims; +use actix_web::{ + Error, HttpMessage, body::MessageBody, dev::ServiceRequest, dev::ServiceResponse, + middleware::Next, +}; // 新增:用于包装中间件函数 + +// 中间件函数:泛型 B 约束为 MessageBody,返回 Result, Error> +pub async fn jwt_middleware( + req: ServiceRequest, + next: Next, +) -> Result, Error> { + // (保持原有逻辑不变) + let auth_header = req + .headers() + .get("Authorization") + .ok_or_else(|| actix_web::error::ErrorUnauthorized("缺少Authorization头"))? + .to_str() + .map_err(|_| actix_web::error::ErrorUnauthorized("Authorization格式无效"))?; + + let token = auth_header.strip_prefix("Bearer ").ok_or_else(|| { + actix_web::error::ErrorUnauthorized("Authorization格式应为 Bearer ") + })?; + + let jwt_secret = std::env::var("JWT_SECRET") + .map_err(|_| actix_web::error::ErrorInternalServerError("服务器未配置JWT密钥"))?; + + let claims = verify_token(token, &jwt_secret) + .map_err(|e| actix_web::error::ErrorUnauthorized(format!("无效的token: {}", e)))?; + + req.extensions_mut().insert(claims); + next.call(req).await +} + +// 生成JWT的函数 +pub fn generate_token( + user_id: i32, + openid: &str, + user_type: i32, + secret: &str, +) -> Result { + // 设置过期时间:当前时间 + 24小时(86400秒) + let now = Utc::now(); + let exp = (now + chrono::Duration::hours(24)).timestamp(); + let iat = now.timestamp(); + + // 构建Claims + let claims = Claims { + exp, + iat, + user_id, + openid: openid.to_string(), + user_type, + }; + + // 生成token + encode( + &Header::new(Algorithm::HS256), // 使用HS256算法 + &claims, + &EncodingKey::from_secret(secret.as_bytes()), // 签名密钥 + ) + .map_err(|e| format!("生成JWT失败: {}", e)) +} + +// 验证并解析JWT的函数 +pub fn verify_token(token: &str, secret: &str) -> Result { + // 验证配置(指定算法,默认会检查exp等字段) + let validation = Validation::new(Algorithm::HS256); + + // 解析token + let decoded = decode::( + token, + &DecodingKey::from_secret(secret.as_bytes()), // 与签发时相同的密钥 + &validation, + ) + .map_err(|e| format!("JWT验证失败: {}", e))?; + + Ok(decoded.claims) +} diff --git a/src/main.rs b/src/main.rs index 56ff5bc..ab62146 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,59 +1,19 @@ +use actix_web::middleware::from_fn; use actix_web::{App, HttpResponse, HttpServer, Responder, Result, post, web}; -use db::create_pool; use reqwest::Client; use sqlx::postgres::PgPool; +mod auth; mod db; mod models; -use db::insert_weather_data; -use models::{ - ErrorResponse, OpenIdRequest, OpenIdResponse, UserIdResponse, WeChatApiResponse, - WeChatLoginRequest, WeatherData, -}; +use auth::{generate_token, jwt_middleware}; +use db::{create_pool, insert_weather_data}; +use models::{ErrorResponse, WeChatApiResponse, WeChatLoginRequest, WeatherData}; -// 新增:根据openid获取用户ID的端点 -#[post("/getmyid")] -async fn get_user_id(pool: web::Data, request: web::Json) -> impl Responder { - let openid = &request.openid; - - // 查询用户ID - let query = r#" - SELECT id FROM users WHERE openid = $1 - "#; - - let result: Result, sqlx::Error> = sqlx::query_as::<_, (i32,)>(query) - .bind(openid) - .fetch_optional(pool.get_ref()) - .await; - - match result { - Ok(Some(user_id)) => { - println!("Found user ID {} for openid {}", user_id.0, openid); - HttpResponse::Ok().json(UserIdResponse { user_id: user_id.0 }) - } - Ok(None) => { - println!("No user found with openid: {}", openid); - HttpResponse::NotFound().json(ErrorResponse { - error: format!("未找到openid为 {} 的用户", openid), - errcode: Some(404), // 添加错误码 - errmsg: Some("用户不存在".to_string()), // 添加错误消息 - }) - } - Err(e) => { - eprintln!("Database error: {}", e); - HttpResponse::InternalServerError().json(ErrorResponse { - error: format!("数据库查询错误: {}", e), - errcode: Some(500), // 添加错误码 - errmsg: Some(e.to_string()), // 添加错误消息 - }) - } - } -} - -// 获取openid的API端点 -#[post("/getopenid")] -async fn get_openid( +// 登录的API端点 +#[post("/api/login")] +async fn login( pool: web::Data, req: web::Json, http_client: web::Data, @@ -121,6 +81,7 @@ async fn get_openid( errmsg: wechat_data.errmsg, }); } + // 获取openid let openid = match wechat_data.openid { Some(id) => id, @@ -133,36 +94,75 @@ async fn get_openid( } }; - // 检查用户是否已存在,不存在则创建 + // 3. 检查用户是否已存在,不存在则创建(显式处理数据库错误) let query = r#" - INSERT INTO users (openid) - VALUES ($1) - ON CONFLICT (openid) DO NOTHING - RETURNING id - "#; + INSERT INTO users (openid, name, type) + VALUES ($1, left($1, 8), 2) + ON CONFLICT (openid) DO NOTHING + RETURNING id +"#; - let result = sqlx::query_as::<_, (i32,)>(query) + let user_id = match sqlx::query_as::<_, (i32,)>(query) .bind(&openid) .fetch_optional(pool.get_ref()) - .await; - match result { - Ok(Some(user_id)) => { - println!("用户已存在,ID: {}", user_id.0); + .await + { + Ok(Some((id,))) => { + // 已存在用户,直接获取ID + println!("用户已存在,ID: {}", id); + id } Ok(None) => { + // 新用户创建成功,需再次查询ID(因ON CONFLICT DO NOTHING不返回值) println!("新用户已创建,openid: {}", openid); + match sqlx::query_as::<_, (i32,)>("SELECT id FROM users WHERE openid = $1") + .bind(&openid) + .fetch_one(pool.get_ref()) + .await + { + Ok((id,)) => id, + Err(e) => { + eprintln!("查询新用户ID失败: {}", e); + return HttpResponse::InternalServerError().json(ErrorResponse { + error: "创建用户后查询ID失败".to_string(), + errcode: Some(500), + errmsg: Some(e.to_string()), + }); + } + } } Err(e) => { - eprintln!("数据库操作失败: {}", e); - // 即使数据库操作失败,我们仍然返回openid,因为微信登录已经成功 + // 数据库操作失败 + eprintln!("用户查询/创建失败: {}", e); + return HttpResponse::InternalServerError().json(ErrorResponse { + error: "用户信息处理失败".to_string(), + errcode: Some(500), + errmsg: Some(e.to_string()), + }); } - } + }; - // 返回openid给前端 - HttpResponse::Ok().json(OpenIdResponse { openid }) + // 4. 生成JWT token(显式处理所有可能的错误) + // 4.1 获取JWT密钥 + let jwt_secret = match std::env::var("JWT_SECRET") { + Ok(secret) => secret, + Err(_) => { + return HttpResponse::InternalServerError().json(ErrorResponse { + error: "服务器配置错误:缺少JWT_SECRET".to_string(), + errcode: Some(500), + errmsg: None, + }); + } + }; + + // 4.2 编码生成token + let token = generate_token(user_id, &openid, 2, &jwt_secret); + + // 5. 返回成功响应(包含token) + HttpResponse::Ok().json(token) } -#[post("/weather-test")] +#[post("/api/post-weather-data")] async fn post_weather_data( data: web::Json, pool: web::Data, @@ -215,9 +215,14 @@ fn create_server_config( App::new() .app_data(web::Data::new(pool)) .app_data(web::Data::new(http_client)) - .service(get_user_id) - .service(get_openid) - .service(post_weather_data) + // 公开接口(无需验证) + .service(login) + // 需要验证的接口(使用 from_fn 包装中间件) + .service( + web::scope("") + .wrap(from_fn(jwt_middleware)) // 关键修改:用 from_fn 包装 + .service(post_weather_data), + ) } #[actix_web::main] diff --git a/src/models.rs b/src/models.rs index 7052bfe..2faf4a7 100644 --- a/src/models.rs +++ b/src/models.rs @@ -2,15 +2,19 @@ use chrono::NaiveDate; use serde::{Deserialize, Serialize}; use sqlx::FromRow; -// 新增:API请求/响应结构体 -#[derive(Debug, Deserialize)] -pub struct OpenIdRequest { - pub openid: String, -} - -#[derive(Debug, Serialize)] -pub struct UserIdResponse { +// 定义JWT载荷结构体 +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + // 标准字段:过期时间(必须) + pub exp: i64, // 时间戳(秒) + // 标准字段:签发时间 + pub iat: i64, + // 自定义字段:用户ID(根据业务需求添加) pub user_id: i32, + // 自定义字段:openid(可选) + pub openid: String, + // 自定义字段:user_ytpe + pub user_type: i32, } #[derive(Debug, Deserialize)]