use chrono::{Utc, Duration}; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode}; use uuid::Uuid; use crate::error::AppError; use crate::models::{Claims, TempTokenClaims}; use actix_web::{ Error, HttpMessage, body::MessageBody, dev::ServiceRequest, dev::ServiceResponse, middleware::Next, }; // 常量 const ACCESS_TOKEN_EXPIRE_HOURS: i64 = 24; pub async fn jwt_middleware( req: ServiceRequest, next: Next, ) -> Result, Error> { let auth_header = req .headers() .get("Authorization") .ok_or_else(|| AppError::Unauthorized("请先登录".to_string()))? .to_str() .map_err(|_| AppError::Unauthorized("Authorization格式无效".to_string()))?; let token = auth_header.strip_prefix("Bearer ").ok_or_else(|| { AppError::Unauthorized("请使用有效的登录凭证".to_string()) })?; let jwt_secret = std::env::var("JWT_SECRET") .map_err(|_| AppError::Internal("服务器配置错误".to_string()))?; let claims = verify_token(token, &jwt_secret) .map_err(|e| AppError::Unauthorized(format!("登录已过期,请重新登录: {}", e)))?; req.extensions_mut().insert(claims); next.call(req).await } // 生成 access_token pub fn generate_token( user_id: i32, openid: &str, user_type: i32, secret: &str, ) -> Result { let now = Utc::now(); let exp = (now + Duration::hours(ACCESS_TOKEN_EXPIRE_HOURS)).timestamp(); let iat = now.timestamp(); let claims = Claims { exp, iat, user_id, openid: openid.to_string(), user_type, }; encode( &Header::new(Algorithm::HS256), &claims, &EncodingKey::from_secret(secret.as_bytes()), ) .map_err(|e| format!("生成JWT失败: {}", e)) } // 生成 refresh_token(随机 UUID,不含任何敏感信息) // 身份验证通过数据库查询该 token 是否存在实现 pub fn generate_refresh_token(_user_id: i32, _secret: &str) -> Result { Ok(Uuid::new_v4().to_string()) } // 验证并解析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) } // 生成临时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) } #[cfg(test)] mod tests { use super::*; const TEST_SECRET: &str = "unit-test-secret-0123456789"; #[test] fn test_generate_and_verify_token_roundtrip() { let token = generate_token(42, "openid_test_123", 2, TEST_SECRET).unwrap(); let claims = verify_token(&token, TEST_SECRET).unwrap(); assert_eq!(claims.user_id, 42); assert_eq!(claims.openid, "openid_test_123"); assert_eq!(claims.user_type, 2); assert!(claims.exp > claims.iat, "过期时间应晚于签发时间"); assert!(claims.exp > Utc::now().timestamp(), "令牌应未过期"); } #[test] fn test_verify_token_rejects_wrong_secret() { let token = generate_token(42, "openid", 2, TEST_SECRET).unwrap(); assert!(verify_token(&token, "wrong-secret").is_err()); } #[test] fn test_verify_token_rejects_tampered_payload() { let token = generate_token(42, "openid", 2, TEST_SECRET).unwrap(); // 篡改 token 的载荷部分(第 2 段),应验证失败 let mut parts: Vec<&str> = token.split('.').collect(); assert_eq!(parts.len(), 3); parts[1] = "eyJ1c2VyX2lkIjo5OTl9"; let tampered = parts.join("."); assert!(verify_token(&tampered, TEST_SECRET).is_err()); } #[test] fn test_verify_token_rejects_garbage() { assert!(verify_token("not-a-jwt", TEST_SECRET).is_err()); assert!(verify_token("", TEST_SECRET).is_err()); } #[test] fn test_generate_refresh_token_is_random_uuid() { let a = generate_refresh_token(1, TEST_SECRET).unwrap(); let b = generate_refresh_token(1, TEST_SECRET).unwrap(); assert_ne!(a, b, "两次生成的 refresh token 不应相同"); // UUID v4 格式:8-4-4-4-12 十六进制 let parts: Vec<&str> = a.split('-').collect(); assert_eq!(parts.len(), 5); assert!(parts.iter().all(|p| !p.is_empty())); } #[test] fn test_temp_token_roundtrip_and_expiry_window() { let token = generate_temp_token("openid_share", 777, TEST_SECRET, 10).unwrap(); let claims = verify_temp_token(&token, TEST_SECRET).unwrap(); assert_eq!(claims.openid, "openid_share"); assert_eq!(claims.resource_id, 777); // 10 分钟有效期:exp - iat 约等于 600 秒(允许 ±2 秒误差) let lifetime = claims.exp - claims.iat; assert!((598..=602).contains(&lifetime), "临时 token 有效期应为 10 分钟,实际 {lifetime}s"); assert!(verify_temp_token(&token, "wrong-secret").is_err()); } }