Files
asd-backend/src/auth.rs
Milky0217 e14c85436b feat(auth): 添加 Refresh Token 双 Token 机制
- 添加 /api/refresh-token 接口支持 Token 续期
- 登录接口返回 access_token 和 refresh_token
- 新增 refresh_tokens 表存储 refresh_token
- 部署脚本添加数据库备份和迁移功能
- deploy.sh 添加 4 项 API 测试
- 更新 AGENTS.md 文档
2026-04-19 16:01:41 +08:00

159 lines
4.7 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 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)
}