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:
180
src/error.rs
Normal file
180
src/error.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
use actix_web::{ResponseError, http::StatusCode, HttpResponse};
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ErrorResponse<T = ()> {
|
||||
pub success: bool,
|
||||
pub data: Option<T>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> ErrorResponse<T> {
|
||||
pub fn success(data: T) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
data: Some(data),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(msg: impl Into<String>) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some(msg.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> ErrorResponse<T> {
|
||||
pub fn to_json_response(self) -> HttpResponse {
|
||||
HttpResponse::Ok().json(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorResponse<()> {
|
||||
pub fn to_error_response(&self, status: StatusCode) -> HttpResponse {
|
||||
let body = serde_json::json!({
|
||||
"success": false,
|
||||
"error": self.error.clone().unwrap_or_else(|| "Unknown error".to_string())
|
||||
});
|
||||
HttpResponse::build(status).json(body)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AppError {
|
||||
Unauthorized(String),
|
||||
Forbidden(String),
|
||||
NotFound(String),
|
||||
BadRequest(String),
|
||||
Internal(String),
|
||||
Database(String),
|
||||
TooManyRequests(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for AppError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
AppError::Unauthorized(msg) => write!(f, "{}", msg),
|
||||
AppError::Forbidden(msg) => write!(f, "{}", msg),
|
||||
AppError::NotFound(msg) => write!(f, "{}", msg),
|
||||
AppError::BadRequest(msg) => write!(f, "{}", msg),
|
||||
AppError::Internal(msg) => write!(f, "{}", msg),
|
||||
AppError::Database(msg) => write!(f, "{}", msg),
|
||||
AppError::TooManyRequests(msg) => write!(f, "{}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for AppError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
AppError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
|
||||
AppError::Forbidden(_) => StatusCode::FORBIDDEN,
|
||||
AppError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||
AppError::BadRequest(_) => StatusCode::BAD_REQUEST,
|
||||
AppError::Internal(_) | AppError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
AppError::TooManyRequests(_) => StatusCode::TOO_MANY_REQUESTS,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> actix_web::HttpResponse {
|
||||
let error_message = self.to_string();
|
||||
let status_code = self.status_code().as_u16();
|
||||
|
||||
let html = format!(r#"<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>错误 {status_code}</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, #e74c3c 0%, #c0392b 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;
|
||||
}}
|
||||
.error-code {{
|
||||
font-size: 72px;
|
||||
font-weight: bold;
|
||||
color: #e74c3c;
|
||||
margin-bottom: 16px;
|
||||
}}
|
||||
h1 {{
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
font-size: 24px;
|
||||
}}
|
||||
.message {{
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin: 24px 0;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}}
|
||||
.hint {{
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
margin-top: 24px;
|
||||
}}
|
||||
.back-link {{
|
||||
display: inline-block;
|
||||
margin-top: 24px;
|
||||
padding: 12px 24px;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: background 0.3s;
|
||||
}}
|
||||
.back-link:hover {{
|
||||
background: #2980b9;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="error-code">{status_code}</div>
|
||||
<h1>{error_title}</h1>
|
||||
<div class="message">{error_message}</div>
|
||||
<p style="margin-top: 24px; color: #3498db; font-size: 14px;">请返回小程序重新生成新的下载链接</p>
|
||||
<div class="hint">如果问题持续存在,请联系技术支持</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#,
|
||||
error_title = match self {
|
||||
AppError::Unauthorized(_) => "认证失败",
|
||||
AppError::Forbidden(_) => "权限不足",
|
||||
AppError::NotFound(_) => "内容未找到",
|
||||
AppError::BadRequest(_) => "请求无效",
|
||||
AppError::Internal(_) | AppError::Database(_) => "服务器错误",
|
||||
AppError::TooManyRequests(_) => "请求过于频繁",
|
||||
}
|
||||
);
|
||||
|
||||
HttpResponse::build(self.status_code())
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(html)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user