diff --git a/Cargo.lock b/Cargo.lock index 869633c..f874137 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1181,6 +1181,25 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "2.11.0" @@ -1860,6 +1879,7 @@ dependencies = [ "chrono", "dotenvy", "error", + "include_dir", "jsonwebtoken", "reqwest", "serde", diff --git a/Cargo.toml b/Cargo.toml index bcb174a..e1f8f78 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" +include_dir = "0.7.4" jsonwebtoken = "9.3.1" reqwest = { version = "0.12.23", features=["json"]} serde = { version = "1.0.219", features = ["derive"] } diff --git a/src/auth.rs b/src/auth.rs index 74fdcb3..a254632 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,7 +1,7 @@ use chrono::Utc; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode}; -use crate::models::Claims; +use crate::models::{Claims, TempTokenClaims}; use actix_web::{ Error, HttpMessage, body::MessageBody, dev::ServiceRequest, dev::ServiceResponse, middleware::Next, @@ -79,3 +79,42 @@ pub fn verify_token(token: &str, secret: &str) -> Result { Ok(decoded.claims) } + +// 生成临时token(包含openid和resource_id,短期有效) +pub fn generate_temp_token( + openid: &str, + resource_id: i32, + secret: &str, + expire_minutes: i64, // 有效期(分钟) +) -> Result { + let now = Utc::now(); + let exp = (now + chrono::Duration::minutes(expire_minutes)).timestamp(); + let iat = now.timestamp(); + + let claims = TempTokenClaims { + exp, + iat, + openid: openid.to_string(), + resource_id, + }; + + encode( + &Header::new(Algorithm::HS256), + &claims, + &EncodingKey::from_secret(secret.as_bytes()), + ) + .map_err(|e| format!("生成临时token失败: {}", e)) +} + +// 验证临时token并返回载荷 +pub fn verify_temp_token(token: &str, secret: &str) -> Result { + let validation = Validation::new(Algorithm::HS256); + let decoded = decode::( + token, + &DecodingKey::from_secret(secret.as_bytes()), + &validation, + ) + .map_err(|e| format!("临时token验证失败: {}", e))?; + + Ok(decoded.claims) +} diff --git a/src/main.rs b/src/main.rs index 25e4bb8..6e078a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use actix_web::middleware::from_fn; use actix_web::{App, HttpResponse, HttpServer, Responder, delete, get, post, web}; +use include_dir::{Dir, include_dir}; use reqwest::Client; use sqlx::postgres::PgPool; mod auth; @@ -12,6 +13,10 @@ use models::{ErrorResponse, WeChatApiResponse, WeChatLoginRequest, WeatherData}; use crate::models::Claims; +// 编译时嵌入整个 static 目录 +static STATIC_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/static"); +static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates"); + // 登录的API端点 #[post("/api/login")] async fn login( @@ -200,29 +205,138 @@ async fn post_weather_data( } } -// 在main.rs中添加以下处理函数 - -// 获取天气数据详情 -#[get("/weather/details/{id}")] -async fn get_weather_details( +// main.rs +#[post("/api/generate-temp-token/{resource_id}")] +async fn generate_temp_token_handler( path: web::Path, pool: web::Data, - claims: web::ReqData, + claims: web::ReqData, // 依赖原有JWT验证,确保是合法用户 ) -> impl Responder { - let weather_id = path.into_inner(); - println!("获取天气数据详情, ID: {}", weather_id); + let resource_id = path.into_inner(); + let openid: &String = &claims.openid; + // 校验用户是否有权限访问该资源(防止生成其他用户的资源token) + match db::get_weather_details(pool.get_ref(), resource_id).await { + Ok(weather_data) => { + if weather_data.openid != *openid { + return HttpResponse::Forbidden().json(ErrorResponse { + error: "无权限为该资源生成临时token".to_string(), + errcode: Some(403), + errmsg: None, + }); + } + } + Err(e) => { + return HttpResponse::BadRequest().json(ErrorResponse { + error: format!("资源不存在: {}", e), + errcode: Some(404), + errmsg: None, + }); + } + } + + // 获取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, + }); + } + }; + + // 生成临时token(有效期10分钟) + let temp_token = match auth::generate_temp_token(openid, resource_id, &jwt_secret, 10) { + Ok(token) => token, + Err(e) => { + return HttpResponse::InternalServerError().json(ErrorResponse { + error: format!("生成临时token失败: {}", e), + errcode: Some(500), + errmsg: None, + }); + } + }; + + HttpResponse::Ok().json(serde_json::json!({ + "temp_token": temp_token, + "expire_minutes": 10, + })) +} + +// 获取天气数据详情 +// 修改获取天气数据详情接口(移除路径中的id参数) +#[get("/weather/details")] +async fn get_weather_details( + pool: web::Data, + claims: Option>, // 原有JWT的claims变为可选 + query: web::Query, // 用于获取URL参数temp_token和id +) -> impl Responder { + 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, + }); + } + }; + + // 1. 优先检查临时token(URL参数) + let (openid, weather_id) = + if let Some(temp_token) = query.get("temp_token").and_then(|v| v.as_str()) { + // 验证临时token + let temp_claims = match auth::verify_temp_token(temp_token, &jwt_secret) { + Ok(c) => c, + Err(e) => { + return HttpResponse::Unauthorized().json(ErrorResponse { + error: format!("临时token无效: {}", e), + errcode: Some(401), + errmsg: None, + }); + } + }; + + // 从临时token中获取资源ID + (temp_claims.openid, temp_claims.resource_id) + } + // 2. 若没有临时token,使用原有JWT验证(需要查询参数id) + else if let Some(claims) = claims { + // 从查询参数获取id + let weather_id = match query.get("id").and_then(|v| v.as_i64()) { + Some(id) => id as i32, + None => { + return HttpResponse::BadRequest().json(ErrorResponse { + error: "缺少资源ID参数(id)".to_string(), + errcode: Some(400), + errmsg: None, + }); + } + }; + + (claims.openid.clone(), weather_id) + } + // 3. 两种验证方式都没有,返回未授权 + else { + return HttpResponse::Unauthorized().json(ErrorResponse { + error: "缺少token(需提供JWT或临时token)".to_string(), + errcode: Some(401), + errmsg: None, + }); + }; + + // 后续逻辑:校验openid对资源的权限(复用原有逻辑) match db::get_weather_details(pool.get_ref(), weather_id).await { Ok(weather_data) => { - // 检查数据是否属于当前用户 - if weather_data.openid != claims.openid { + if weather_data.openid != openid { return HttpResponse::Forbidden().json(ErrorResponse { error: "无权限访问该数据".to_string(), errcode: Some(403), errmsg: None, }); } - HttpResponse::Ok().json(serde_json::json!({ "success": true, "data": weather_data @@ -362,14 +476,15 @@ fn create_server_config( .app_data(web::Data::new(http_client)) // 公开接口(无需验证) .service(login) + .service(get_weather_details) // 新增 // 需要验证的接口(使用 from_fn 包装中间件) .service( web::scope("") .wrap(from_fn(jwt_middleware)) // 关键修改:用 from_fn 包装 .service(post_weather_data) - .service(get_weather_details) // 新增 .service(get_weather_brief) // 新增 - .service(delete_weather), // 新增 + .service(generate_temp_token_handler) + .service(delete_weather), ) } diff --git a/src/models.rs b/src/models.rs index ee0fab1..0c7f144 100644 --- a/src/models.rs +++ b/src/models.rs @@ -19,6 +19,15 @@ pub struct Claims { pub user_type: i32, } +// 临时访问用 token +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TempTokenClaims { + pub exp: i64, // 过期时间(时间戳) + pub iat: i64, // 签发时间 + pub openid: String, // 用户身份标识 + pub resource_id: i32, // 允许访问的资源ID(如天气数据id) +} + #[derive(Debug, Deserialize)] pub struct WeChatLoginRequest { pub code: String, diff --git a/static/js/afterbody.js b/static/js/afterbody.js index 875cacf..4307112 100644 --- a/static/js/afterbody.js +++ b/static/js/afterbody.js @@ -1,7 +1,5 @@ const version = "0.1.9"; -let weatherdata = null; - //公式相关 function toggleAccordion(header) { @@ -54,7 +52,20 @@ document.getElementById('downloadBtn').addEventListener('click', function() { // 隐藏按钮(而不是移除) btn.style.display = 'none'; - // + // 从 api 中取得 weatherdata + let weatherdata = null; + selectedId = 300; + fetch(`/weather/details?id=${selectedId}`) + .then(response => response.json()) + .then(data => { + //更新全局变量 + weatherdata = data; + }) + .catch(error => { + console.error('Error:', error); + alert('获取详细信息失败'); + }); + const data = weatherdata; const areaTypeMap = { @@ -356,135 +367,6 @@ document.getElementById('downloadBtn').addEventListener('click', function() { // 获取记录内容 document.getElementById('viewDetailsBtn').addEventListener('click', function() { - const selectedRadio = document.querySelector('input[name="selectedData"]:checked'); - if (!selectedRadio) { - alert('请先选择一条数据'); - return; - } - - const selectedId = selectedRadio.value; - fetch(`/weather/details?id=${selectedId}`) - .then(response => response.json()) - .then(data => { - //更新全局变量 - weatherdata = data; - const detailsTable = document.getElementById('detailsTable'); - detailsTable.innerHTML = ''; - - const row = document.createElement('div'); - row.innerHTML = ` -
-
-

${data.title}

-

${data.inspectiontype}

-

${data.assignmentnumber}

-
-
-
- 时间: - ${formatDateToYYYYMMDD(data.date)} ${data.hour}时 ${data.min}分 -
-
- 经纬度: - 位置: 经度 ${data.longitude}, 纬度 ${data.latitude} -
-
- 地球公转角: - ${data.theta} -
-
- 太阳倾角: - ${data.solardeclination} -
-
- 太阳高度角: - ${data.sunaltitude} -
-
- 云量指数:${data.cloudindex} - 低云 ${data.lowcloudiness}, 总云量 ${data.overallcloudiness} -
-
- 太阳辐射等级: - ${data.solarradiationlevel} -
- - - - - -
- 大气稳定度: - ${data.atmosphericstability} -
-
- 适宜度: - ${data.suitabilitydegree} -
- -
- 风速:${data.windspeed} - 平均风速:${data.averagewindspeed} - 适宜度:${data.windspeedsuitability} -
- -
- 风向:${data.winddirection} - 平均风向 ${data.averagewinddirection} 风向标准差 ${data.winddirectionstandarddeviation} - 适宜度:${data.winddirectionsuitability} -
- -
- 整体适宜度: - ${data.overallsuitability} -
-
-
- `; - detailsTable.appendChild(row); - - // 根据条件显示相应的数据 - if (data.hasmeasuredwindspeed==='true') { - document.getElementById('measured-data').style.display = 'block'; - document.getElementById('calculated-data').style.display = 'none'; - document.getElementById('measured-windspeed').textContent = data.measuredwindspeed; - document.getElementById('converted-windspeed').textContent = data.convertedwindspeed; - } else { - document.getElementById('measured-data').style.display = 'none'; - document.getElementById('calculated-data').style.display = 'block'; - document.getElementById('measurement-height').textContent = data.measurementheight; - document.getElementById('area-type').textContent = data.areatype; - document.getElementById('point-windspeed').textContent = data.pointwindspeed; - }; - - document.getElementById('detailsContainer').style.display = 'block'; - }) - .catch(error => { - console.error('Error:', error); - alert('获取详细信息失败'); - }); }); // 工具函数