- 添加 /api/refresh-token 接口支持 Token 续期 - 登录接口返回 access_token 和 refresh_token - 新增 refresh_tokens 表存储 refresh_token - 部署脚本添加数据库备份和迁移功能 - deploy.sh 添加 4 项 API 测试 - 更新 AGENTS.md 文档
159 lines
4.7 KiB
Rust
159 lines
4.7 KiB
Rust
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;
|
||
|
||
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(简单 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等字段)
|
||
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)
|
||
}
|