feat(auth): 实现登录限流和性能优化
性能优化: - 添加数据库索引优化查询性能 (006) - weather_data: user_id, date, is_favorite 索引 - users: openid 索引 - payment_orders: status 索引 - 新增 refresh_tokens 表支持双 Token 机制 (004) - 新增 web_login_codes 表支持网页端扫码登录 (005) 安全增强: - 实现基于 IP 的登录限流 (rate_limiter.rs) - 滑动窗口算法: 5次/分钟/IP - 自动清理过期记录 - 429 TooManyRequests 响应 新模块: - src/rate_limiter.rs: 限流模块 - src/alipay.rs: 支付宝签名模块 (RSA2) - src/error.rs: 统一错误类型 (含 TooManyRequests) - src/handlers/meta.rs: 元数据处理器 代码清理: - 修复 .gitignore 规则,正确跟踪 src/ 和 migrations/
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use actix_web::{web, HttpResponse, Responder, post, get};
|
||||
use actix_web::{web, HttpResponse, HttpRequest, Responder, post, get};
|
||||
use chrono::{Utc, Duration};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use reqwest::Client;
|
||||
@@ -13,6 +13,8 @@ use crate::models::{
|
||||
AppState, LoginResponse, RefreshTokenRequest, TokenRefreshResponse,
|
||||
WeChatApiResponse, WeChatLoginRequest,
|
||||
};
|
||||
use crate::rate_limiter::LOGIN_RATE_LIMITER;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[post("/api/login")]
|
||||
pub async fn login(
|
||||
@@ -20,7 +22,27 @@ pub async fn login(
|
||||
req: web::Json<WeChatLoginRequest>,
|
||||
http_client: web::Data<Client>,
|
||||
app_state: web::Data<AppState>,
|
||||
http_req: HttpRequest,
|
||||
) -> impl Responder {
|
||||
let client_ip = http_req
|
||||
.headers()
|
||||
.get("X-Forwarded-For")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.split(',').next().unwrap_or(s).trim().to_string())
|
||||
.or_else(|| {
|
||||
http_req
|
||||
.headers()
|
||||
.get("X-Real-IP")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
if let Err(e) = LOGIN_RATE_LIMITER.check_rate_limit(&client_ip).await {
|
||||
warn!("登录请求被限流: client_ip={}", client_ip);
|
||||
return HttpResponse::TooManyRequests().json(ErrorResponse::<()>::error(e.to_string()));
|
||||
}
|
||||
|
||||
let url = format!(
|
||||
"https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code",
|
||||
app_state.wechat_appid, app_state.wechat_secret, req.code
|
||||
|
||||
112
src/handlers/meta.rs
Normal file
112
src/handlers/meta.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use actix_web::{get, web, HttpResponse, Responder};
|
||||
use sqlx::postgres::PgPool;
|
||||
|
||||
const HTML_TEMPLATE: &str = r#"<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>大气稳定度判定系统</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 48px;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
font-size: 28px;
|
||||
}
|
||||
.subtitle {
|
||||
color: #666;
|
||||
margin-bottom: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.status-box {
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.status-ok {
|
||||
background: #d4edda;
|
||||
border: 1px solid #28a745;
|
||||
}
|
||||
.status-error {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #dc3545;
|
||||
}
|
||||
.status-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.status-text {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.status-ok .status-text { color: #155724; }
|
||||
.status-error .status-text { color: #721c24; }
|
||||
.status-detail {
|
||||
color: #495057;
|
||||
font-size: 14px;
|
||||
}
|
||||
.version {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>大气稳定度判定系统</h1>
|
||||
<p class="subtitle">后端服务状态</p>
|
||||
|
||||
<div class="status-box {status_class}">
|
||||
<div class="status-icon">{status_icon}</div>
|
||||
<div class="status-text">{status_text}</div>
|
||||
<div class="status-detail">
|
||||
<div>数据库: {database}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="version">v{version}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#;
|
||||
|
||||
#[get("/")]
|
||||
pub async fn root(pool: web::Data<PgPool>) -> impl Responder {
|
||||
let (status_class, status_icon, status_text, database) = match sqlx::query("SELECT 1")
|
||||
.fetch_one(pool.get_ref())
|
||||
.await
|
||||
{
|
||||
Ok(_) => ("status-ok", "✅", "服务正常", "已连接"),
|
||||
Err(_) => ("status-error", "❌", "服务异常", "未连接"),
|
||||
};
|
||||
|
||||
let html = HTML_TEMPLATE
|
||||
.replace("{status_class}", status_class)
|
||||
.replace("{status_icon}", status_icon)
|
||||
.replace("{status_text}", status_text)
|
||||
.replace("{database}", database)
|
||||
.replace("{version}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(html)
|
||||
}
|
||||
Reference in New Issue
Block a user