- auth.rs 新增 JWT 生成/验证回环、篡改拒绝、refresh token 随机性、临时 token 有效期测试 - payment.rs 新增套餐定义、URL 解码、支付宝表单解析(UTF-8/GBK)、RSA2 签名验签、UA 检测测试 - 修复 urlencoding 两个真实 bug:UTF-8 多字节序列乱码、不完整转义吞字符 - 移除 db.rs 悬空 doc 注释,修复 auth.rs 冗余引用(clippy 0 警告) - cargo test 从 2 个增至 36 个全过 - 同步 IMPROVEMENTS.md(索引/重复查询/测试覆盖/公告系统状态)
195 lines
6.2 KiB
Rust
195 lines
6.2 KiB
Rust
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<B: MessageBody>(
|
||
req: ServiceRequest,
|
||
next: Next<B>,
|
||
) -> Result<ServiceResponse<B>, 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<String, String> {
|
||
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<String, String> {
|
||
Ok(Uuid::new_v4().to_string())
|
||
}
|
||
|
||
// 验证并解析JWT的函数
|
||
pub fn verify_token(token: &str, secret: &str) -> Result<Claims, String> {
|
||
// 验证配置(指定算法,默认会检查exp等字段)
|
||
let validation = Validation::new(Algorithm::HS256);
|
||
|
||
// 解析token
|
||
let decoded = decode::<Claims>(
|
||
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<String, String> {
|
||
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<TempTokenClaims, String> {
|
||
let validation = Validation::new(Algorithm::HS256);
|
||
let decoded = decode::<TempTokenClaims>(
|
||
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());
|
||
}
|
||
}
|