feat(auth): 添加 Refresh Token 双 Token 机制

- 添加 /api/refresh-token 接口支持 Token 续期
- 登录接口返回 access_token 和 refresh_token
- 新增 refresh_tokens 表存储 refresh_token
- 部署脚本添加数据库备份和迁移功能
- deploy.sh 添加 4 项 API 测试
- 更新 AGENTS.md 文档
This commit is contained in:
2026-04-19 16:01:41 +08:00
parent 52991dcfd5
commit e14c85436b
11 changed files with 872 additions and 98 deletions

View File

@@ -1,52 +1,54 @@
use chrono::Utc;
use chrono::{Utc, Duration};
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
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;
const REFRESH_TOKEN_EXPIRE_DAYS: i64 = 7;
// 中间件函数:泛型 B 约束为 MessageBody返回 Result<ServiceResponse<B>, Error>
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(|| actix_web::error::ErrorUnauthorized("缺少Authorization头"))?
.ok_or_else(|| AppError::Unauthorized("请先登录".to_string()))?
.to_str()
.map_err(|_| actix_web::error::ErrorUnauthorized("Authorization格式无效"))?;
.map_err(|_| AppError::Unauthorized("Authorization格式无效".to_string()))?;
let token = auth_header.strip_prefix("Bearer ").ok_or_else(|| {
actix_web::error::ErrorUnauthorized("Authorization格式应为 Bearer <token>")
AppError::Unauthorized("请使用有效的登录凭证".to_string())
})?;
let jwt_secret = std::env::var("JWT_SECRET")
.map_err(|_| actix_web::error::ErrorInternalServerError("服务器配置JWT密钥"))?;
.map_err(|_| AppError::Internal("服务器配置错误".to_string()))?;
let claims = verify_token(token, &jwt_secret)
.map_err(|e| actix_web::error::ErrorUnauthorized(format!("无效的token: {}", e)))?;
.map_err(|e| AppError::Unauthorized(format!("登录已过期,请重新登录: {}", e)))?;
req.extensions_mut().insert(claims);
next.call(req).await
}
// 生成JWT的函数
// 生成 access_token
pub fn generate_token(
user_id: i32,
openid: &str,
user_type: i32,
secret: &str,
) -> Result<String, String> {
// 设置过期时间:当前时间 + 24小时86400秒
let now = Utc::now();
let exp = (now + chrono::Duration::hours(24)).timestamp();
let exp = (now + Duration::hours(ACCESS_TOKEN_EXPIRE_HOURS)).timestamp();
let iat = now.timestamp();
// 构建Claims
let claims = Claims {
exp,
iat,
@@ -55,15 +57,51 @@ pub fn generate_token(
user_type,
};
// 生成token
encode(
&Header::new(Algorithm::HS256), // 使用HS256算法
&Header::new(Algorithm::HS256),
&claims,
&EncodingKey::from_secret(secret.as_bytes()), // 签名密钥
&EncodingKey::from_secret(secret.as_bytes()),
)
.map_err(|e| format!("生成JWT失败: {}", e))
}
// 生成 refresh_token简单 base64 编码的随机字符串)
pub fn generate_refresh_token(user_id: i32, secret: &str) -> Result<String, String> {
let now = Utc::now();
let exp = (now + Duration::days(REFRESH_TOKEN_EXPIRE_DAYS)).timestamp();
let payload = format!("{}:{}:{}", user_id, exp, secret);
let token = BASE64.encode(payload.as_bytes());
Ok(token)
}
// 解析 refresh_token返回 (user_id, expires_at)
pub fn verify_refresh_token(token: &str, secret: &str) -> Result<(i32, i64), String> {
let decoded = BASE64.decode(token)
.map_err(|e| format!("Refresh token 格式错误: {}", e))?;
let payload = String::from_utf8(decoded)
.map_err(|_| "Refresh token 解析失败".to_string())?;
let parts: Vec<&str> = payload.split(':').collect();
if parts.len() != 3 {
return Err("Refresh token 结构错误".to_string());
}
let user_id: i32 = parts[0].parse()
.map_err(|_| "Refresh token user_id 解析失败".to_string())?;
let exp: i64 = parts[1].parse()
.map_err(|_| "Refresh token exp 解析失败".to_string())?;
let now = Utc::now().timestamp();
if now > exp {
return Err("Refresh token 已过期".to_string());
}
Ok((user_id, exp))
}
// 验证并解析JWT的函数
pub fn verify_token(token: &str, secret: &str) -> Result<Claims, String> {
// 验证配置指定算法默认会检查exp等字段