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,11 +1,15 @@
use actix_web::{web, HttpResponse, Responder, post};
use chrono::{Utc, Duration};
use reqwest::Client;
use sqlx::postgres::PgPool;
use tracing::{debug, error, info, warn};
use crate::auth::generate_token;
use crate::auth::{generate_token, generate_refresh_token, verify_refresh_token};
use crate::db;
use crate::error::ErrorResponse;
use crate::models::{
AppState, ErrorResponse, TokenResponse, WeChatApiResponse, WeChatLoginRequest,
AppState, LoginResponse, RefreshTokenRequest, TokenRefreshResponse,
WeChatApiResponse, WeChatLoginRequest,
};
#[post("/api/login")]
@@ -28,11 +32,7 @@ pub async fn login(
}
Err(e) => {
error!("微信API请求失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "请求微信服务失败".to_string(),
errcode: None,
errmsg: None,
});
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("请求微信服务失败"));
}
};
@@ -43,11 +43,7 @@ pub async fn login(
}
Err(e) => {
error!("微信API响应解析失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "解析微信响应失败".to_string(),
errcode: None,
errmsg: None,
});
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("解析微信响应失败"));
}
};
@@ -56,11 +52,11 @@ pub async fn login(
"微信登录验证失败 - 错误码: {}, 错误信息: {:?}",
errcode, wechat_data.errmsg
);
return HttpResponse::BadRequest().json(ErrorResponse {
error: "微信登录失败".to_string(),
errcode: Some(errcode),
errmsg: wechat_data.errmsg,
});
let errmsg = wechat_data.errmsg.unwrap_or_default();
return HttpResponse::BadRequest().json(ErrorResponse::<()>::error(format!(
"微信登录失败: {} {}",
errcode, errmsg
)));
}
let openid = match wechat_data.openid {
@@ -71,11 +67,7 @@ pub async fn login(
}
None => {
error!("微信API响应中未包含openid");
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "微信API未返回openid".to_string(),
errcode: None,
errmsg: None,
});
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("微信API未返回openid"));
}
};
@@ -97,11 +89,7 @@ pub async fn login(
}
Err(e) => {
error!("用户信息处理失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "用户信息处理失败".to_string(),
errcode: Some(500),
errmsg: None,
});
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("用户信息处理失败"));
}
};
@@ -112,14 +100,93 @@ pub async fn login(
}
Err(e) => {
error!("JWT生成失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: "生成身份令牌失败".to_string(),
errcode: Some(500),
errmsg: None,
});
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("生成身份令牌失败"));
}
};
let token_response = TokenResponse::new(token);
HttpResponse::Ok().json(token_response)
let refresh_token_str = match generate_refresh_token(user_id, &app_state.jwt_secret) {
Ok(t) => t,
Err(e) => {
error!("Refresh token 生成失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("生成刷新令牌失败"));
}
};
let refresh_expires_at = Utc::now() + Duration::days(7);
if let Err(e) = db::create_refresh_token(pool.get_ref(), user_id, &refresh_token_str, refresh_expires_at).await {
error!("保存 refresh_token 失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("保存刷新令牌失败"));
}
let login_response = LoginResponse::new(token, refresh_token_str);
HttpResponse::Ok().json(login_response)
}
#[post("/api/refresh-token")]
pub async fn refresh_token(
pool: web::Data<PgPool>,
req: web::Json<RefreshTokenRequest>,
app_state: web::Data<AppState>,
) -> impl Responder {
let refresh_token = &req.refresh_token;
let refresh_token_record = match db::verify_refresh_token(pool.get_ref(), refresh_token).await {
Ok(record) => record,
Err(e) => {
warn!("Refresh token 验证失败: {}", e);
return HttpResponse::Unauthorized().json(ErrorResponse::<()>::error("Refresh token 无效或已过期"));
}
};
let user_id = refresh_token_record.user_id;
let user_query = r#"SELECT openid FROM users WHERE id = $1"#;
let openid: String = match sqlx::query_as::<_, (String,)>(user_query)
.bind(user_id)
.fetch_optional(pool.get_ref())
.await
{
Ok(Some((openid,))) => openid,
Ok(None) => {
error!("用户不存在: {}", user_id);
return HttpResponse::Unauthorized().json(ErrorResponse::<()>::error("用户不存在"));
}
Err(e) => {
error!("查询用户失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("查询用户信息失败"));
}
};
let new_access_token = match generate_token(user_id, &openid, 2, &app_state.jwt_secret) {
Ok(t) => t,
Err(e) => {
error!("生成新 JWT 失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("生成令牌失败"));
}
};
let new_refresh_token = match generate_refresh_token(user_id, &app_state.jwt_secret) {
Ok(t) => t,
Err(e) => {
error!("生成新 refresh_token 失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("生成刷新令牌失败"));
}
};
if let Err(e) = db::delete_specific_refresh_token(pool.get_ref(), refresh_token).await {
warn!("删除旧 refresh_token 失败(继续): {}", e);
}
let new_expires_at = Utc::now() + Duration::days(7);
if let Err(e) = db::create_refresh_token(pool.get_ref(), user_id, &new_refresh_token, new_expires_at).await {
error!("保存新 refresh_token 失败: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("保存刷新令牌失败"));
}
let response = TokenRefreshResponse {
success: true,
token: new_access_token,
refresh_token: new_refresh_token,
};
HttpResponse::Ok().json(response)
}