今天我們為個人部落格加入簡單的的安全機制:
# Cargo.toml
[dependencies]
# 現有依賴...
# JWT 認證
jsonwebtoken = "9"
# 密碼雜湊
bcrypt = "0.18"
# 時間處理
time = "0.3"
建立 src/auth.rs
:
use std::env;
use anyhow::{Context, Result};
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use chrono::{Duration, Utc};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub sub: String, // 用戶 ID
pub exp: usize, // 過期時間
pub role: String, // 角色
}
#[derive(Debug, Serialize, Deserialize,ToSchema)]
pub struct LoginRequest {
pub username: String,
pub password: String,
}
#[derive(Debug, Serialize, Deserialize,ToSchema)]
pub struct LoginResponse {
pub token: String,
pub expires_in: usize,
}
#[derive(Clone)]
pub struct JwtService {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
validation: Validation,
}
impl JwtService {
pub fn new() -> Result<Self> {
let secret = env::var("JWT_SECRET")
.context("請設定 JWT_SECRET 環境變數")?;
let mut validation = Validation::new(Algorithm::HS256);
validation.validate_exp = true;
Ok(Self {
encoding_key: EncodingKey::from_secret(secret.as_bytes()),
decoding_key: DecodingKey::from_secret(secret.as_bytes()),
validation,
})
}
pub fn generate_token(&self, user_id: &str) -> Result<String> {
let exp = Utc::now() + Duration::hours(24);
let claims = Claims {
sub: user_id.to_string(),
exp: exp.timestamp() as usize,
role: "admin".to_string(),
};
encode(&Header::default(), &claims, &self.encoding_key)
.context("JWT token 生成失敗")
}
pub fn verify_token(&self, token: &str) -> Result<Claims> {
let token_data = decode::<Claims>(token, &self.decoding_key, &self.validation)
.context("JWT token 驗證失敗")?;
Ok(token_data.claims)
}
}
建立 src/auth_middleware.rs
:
use axum::{
extract::{Request, State as AxumState},
http::header::AUTHORIZATION,
middleware::Next,
response::Response,
};
use crate::{error::AppError, state::AppState};
pub async fn auth_middleware(
AxumState(app_state): AxumState<AppState>,
mut request: Request,
next: Next,
) -> Result<Response, AppError> {
let auth_header = request
.headers()
.get(AUTHORIZATION)
.and_then(|header| header.to_str().ok())
.ok_or_else(|| AppError::Unauthorized("缺少認證資訊".to_string()))?;
let token = auth_header
.strip_prefix("Bearer ")
.ok_or_else(|| AppError::Unauthorized("無效的認證格式".to_string()))?;
let claims = app_state
.jwt_service
.verify_token(token)
.map_err(|_| AppError::Unauthorized("無效的認證 token".to_string()))?;
// 將用戶資訊加入請求
request.extensions_mut().insert(claims);
Ok(next.run(request).await)
}
建立 src/auth_handlers.rs
:
use crate::{
auth::{Claims, LoginRequest, LoginResponse},
error::AppError,
state::AppState,
};
use axum::{extract::{State, Extension}, Json};
use std::env;
/// 管理員登入取得 JWT
#[utoipa::path(
post,
path = "/api/admin/login",
tag = "admin",
request_body = LoginRequest,
responses(
(status = 200, description = "登入成功,回傳 JWT", body = LoginResponse),
(status = 401, description = "用戶名或密碼錯誤")
)
)]
pub async fn admin_login(
State(app_state): State<AppState>,
Json(login_request): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, AppError> {
// 簡單的管理員驗證
let admin_username = env::var("ADMIN_USERNAME")
.unwrap_or_else(|_| "admin".to_string());
let admin_password = env::var("ADMIN_PASSWORD")
.unwrap_or_else(|_| "admin123".to_string());
// 驗證用戶名和密碼
if login_request.username != admin_username || login_request.password != admin_password {
return Err(AppError::Unauthorized("用戶名或密碼錯誤".to_string()));
}
// 生成 JWT token
let token = app_state
.jwt_service
.generate_token(&admin_username)
.map_err(|_| AppError::InternalServerError("Token 生成失敗".to_string()))?;
Ok(Json(LoginResponse {
token,
expires_in: 24 * 3600, // 24 小時
}))
}
/// 取得當前管理員資訊
#[utoipa::path(
get,
path = "/api/admin/info",
tag = "admin",
security(("bearer_auth" = [])),
responses(
(status = 200, description = "當前管理員資訊", body = serde_json::Value),
(status = 401, description = "未授權")
)
)]
pub async fn admin_info(Extension(claims): Extension<Claims>) -> Json<serde_json::Value> {
Json(serde_json::json!({
"user_id": claims.sub,
"role": claims.role,
"expires_at": claims.exp
}))
}
建立 src/rate_limit.rs
:
use std::{
collections::HashMap,
net::IpAddr,
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use axum::{
extract::{ConnectInfo, Request},
http::StatusCode,
middleware::Next,
response::Response,
};
#[derive(Debug)]
struct RateLimitEntry {
count: u32,
window_start: Instant,
}
type RateLimitStore = Arc<Mutex<HashMap<IpAddr, RateLimitEntry>>>;
#[derive(Clone)]
pub struct RateLimiter {
store: RateLimitStore,
max_requests: u32,
}
impl RateLimiter {
pub fn new(max_requests: u32) -> Self {
Self {
store: Arc::new(Mutex::new(HashMap::new())),
max_requests,
}
}
fn check_rate_limit(&self, ip: IpAddr) -> bool {
let mut store = self.store.lock().unwrap();
let now = Instant::now();
let window_duration = Duration::from_secs(60); // 1 分鐘窗口
match store.get_mut(&ip) {
Some(entry) => {
// 如果窗口已過期,重置計數
if now.duration_since(entry.window_start) >= window_duration {
entry.count = 1;
entry.window_start = now;
true
} else if entry.count >= self.max_requests {
false // 超過限制
} else {
entry.count += 1;
true
}
}
None => {
// 新 IP
store.insert(
ip,
RateLimitEntry {
count: 1,
window_start: now,
},
);
true
}
}
}
}
pub async fn rate_limit_middleware(
ConnectInfo(addr): ConnectInfo<std::net::SocketAddr>,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
static RATE_LIMITER: std::sync::OnceLock<RateLimiter> = std::sync::OnceLock::new();
let limiter = RATE_LIMITER.get_or_init(|| RateLimiter::new(60)); // 每分鐘 60 次
if !limiter.check_rate_limit(addr.ip()) {
return Err(StatusCode::TOO_MANY_REQUESTS);
}
Ok(next.run(request).await)
}
更新 src/app.rs
:
use crate::{rate_limit::rate_limit_middleware, routes, state::AppState};
use axum::{middleware, Router};
use std::env;
use tower_http::cors::{Any, CorsLayer};
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use crate::docs::ApiDoc;
use crate::auth_middleware::auth_middleware;
pub fn build_app(app_state: AppState) -> Router {
let cors = if env::var("ENVIRONMENT").unwrap_or_default() == "production" {
let allowed_origins = env::var("CORS_ORIGINS")
.unwrap_or_else(|_| "https://yourdomain.com".to_string())
.split(',')
.filter_map(|origin| origin.parse().ok())
.collect::<Vec<_>>();
CorsLayer::new()
.allow_origin(allowed_origins)
.allow_methods([
http::Method::GET,
http::Method::POST,
http::Method::PUT,
http::Method::DELETE,
])
.allow_headers([
http::header::AUTHORIZATION,
http::header::CONTENT_TYPE,
])
} else {
// 開發環境:寬鬆設定
CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any)
};
let public = routes::public_router();
let admin = routes::admin_router();
let admin_protected = admin.layer(middleware::from_fn_with_state(app_state.clone(), auth_middleware));
// Swagger UI 與 OpenAPI 提供路由
let swagger_router = Router::new().merge(
SwaggerUi::new("/docs").url("/api-docs/openapi.json", ApiDoc::openapi()),
);
Router::new()
.merge(swagger_router)
.merge(public)
.nest("/api/admin", admin_protected)
.layer(middleware::from_fn(rate_limit_middleware))
.layer(cors)
.with_state(app_state)
}
更新 src/routes/mod.rs
:
use axum::{routing::get, Router};
use axum::routing::{delete, post, put};
use crate::auth_handler::{admin_info, admin_login};
use crate::auth_middleware::auth_middleware;
use crate::state::AppState;
pub mod blog;
pub mod health;
pub mod posts;
pub mod tags;
pub mod comments;
pub fn public_router() -> Router<AppState> {
Router::new()
.route("/", get(blog::blog_info))
.route("/health", get(health::health_check))
.merge(posts::create_post_routes())
.merge(tags::create_tag_routes())
.merge(comments::create_comment_routes())
// 登入端點為公開
.route("/api/admin/login", post(admin_login))
}
pub fn admin_router() -> Router<AppState> {
Router::new()
.route("/info", get(admin_info))
// posts 管理端點
.route("/posts", post(posts::create_post))
.route("/posts/{id}", get(posts::get_post_for_admin))
.route("/posts/{id}", put(posts::update_post))
.route("/posts/{id}", delete(posts::delete_post))
.route("/posts/search", get(posts::admin_search_posts))
// tags 管理端點
.route("/tags", post(tags::create_tag))
.route("/tags/{id}", get(tags::get_tag_by_id))
.route("/tags/{id}", put(tags::update_tag))
.route("/tags/{id}", delete(tags::delete_tag))
// comments 管理端點
.route("/comments", get(comments::get_admin_comments))
.route("/comments/{id}", put(comments::update_comment_status))
.route("/comments/{id}", delete(comments::delete_comment))
}
更新 src/state.rs
:
use sea_orm::DatabaseConnection;
use crate::cache::CacheConfig;
use crate::cache::post::PostCache;
use crate::cache::tag::TagCache;
use crate::config::Config;
use crate::auth::JwtService;
#[derive(Clone)]
pub struct AppState {
pub db: DatabaseConnection,
pub config: Config,
pub post_cache: PostCache,
pub tag_cache: TagCache,
pub jwt_service: JwtService,
}
impl AppState {
pub fn new(db: DatabaseConnection, config: Config, jwt_service: JwtService) -> Self {
let cache_config = CacheConfig::default();
Self {
db,
config ,
post_cache: PostCache::new(&cache_config),
tag_cache: TagCache::new(&cache_config),
jwt_service,
}
}
}
更新 .env.example
:
# JWT 配置
JWT_SECRET=your-super-secret-jwt-key
JWT_EXPIRY_HOURS=24
# 管理員帳號
ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin123
# CORS 配置
CORS_ORIGINS=http://localhost:3000,https://yourdomain.com
ENVIRONMENT=development
# 其他現有配置...
#[utoipa::path(
post,
path = "/api/admin/posts",
tag = "admin",
security(("bearer_auth" = [])), // 像這樣
request_body = CreatePostRequest,
responses(
(status = 201, description = "文章建立成功", body = PostResponse),
(status = 400, description = "請求資料錯誤"),
(status = 409, description = "Slug 已存在")
)
)]
pub async fn create_post(
State(app_state): State<AppState>,
Json(req): Json<CreatePostRequest>,
) -> Result<(StatusCode, Json<PostResponse>), AppError> {
req.validate().map_err(|e| AppError::ValidationError(e.to_string()))?;
let post = PostService::create_post(&app_state.db, req).await?;
// 新增文章後清理相關快取
app_state.post_cache.invalidate_all().await;
app_state.tag_cache.invalidate_all().await;
info!("📝 文章建立成功,已清理相關快取");
Ok((StatusCode::CREATED, Json(post)))
}
簡潔而有效的安全系統:
JWT 認證:
速率限制:
CORS 設定:
個人部落格特色:
今天就先到此結束拉!明天就是最後一天嘞!!
我們明天見!