Files
asd-backend/src/auth.rs
milky0217 3cce81321f test: 补充单元测试并修复 urlencoding 解码 bug
- 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(索引/重复查询/测试覆盖/公告系统状态)
2026-08-14 13:09:55 +08:00

195 lines
6.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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());
}
}