安全最佳实践 #

Web应用安全是一个持续的过程。本节将介绍Rocket应用开发中的安全最佳实践。

输入验证 #

类型安全验证 #

rust
use rocket::form::{self, FromForm};

#[derive(FromForm)]
struct UserInput {
    #[field(validate = len(3..=50))]
    username: String,
    
    #[field(validate = len(8..))]
    password: String,
    
    #[field(validate = contains('@'))]
    email: String,
    
    #[field(validate = range(0..=150))]
    age: u8,
}

自定义验证器 #

rust
fn validate_username(username: &str) -> form::Result<'_, ()> {
    if username.len() < 3 {
        return Err(form::Error::validation("Username too short").into());
    }
    if !username.chars().all(|c| c.is_alphanumeric() || c == '_') {
        return Err(form::Error::validation("Invalid characters").into());
    }
    Ok(())
}

#[derive(FromForm)]
struct RegisterForm {
    #[field(validate = validate_username)]
    username: String,
}

SQL注入防护 #

rust
use sqlx::query_as;

#[get("/users/<id>")]
async fn get_user(mut db: Connection<Db>, id: i32) -> Option<Json<User>> {
    query_as!(
        User,
        "SELECT * FROM users WHERE id = $1",
        id
    )
    .fetch_optional(&mut **db)
    .await
    .ok()
    .flatten()
    .map(Json)
}

安全头部 #

安全头部Fairing #

rust
use rocket::{Request, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Header;

pub struct SecurityHeaders;

#[rocket::async_trait]
impl Fairing for SecurityHeaders {
    fn info(&self) -> Info {
        Info {
            name: "Security Headers",
            kind: Kind::Response,
        }
    }

    async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
        response.set_header(Header::new("X-Content-Type-Options", "nosniff"));
        response.set_header(Header::new("X-Frame-Options", "DENY"));
        response.set_header(Header::new("X-XSS-Protection", "1; mode=block"));
        response.set_header(Header::new("Strict-Transport-Security", "max-age=31536000; includeSubDomains"));
        response.set_header(Header::new("Content-Security-Policy", "default-src 'self'"));
        response.set_header(Header::new("Referrer-Policy", "strict-origin-when-cross-origin"));
    }
}

安全头部说明 #

头部 作用
X-Content-Type-Options 防止MIME类型嗅探
X-Frame-Options 防止点击劫持
X-XSS-Protection XSS过滤器
Strict-Transport-Security 强制HTTPS
Content-Security-Policy 内容安全策略
Referrer-Policy 控制Referrer信息

密码安全 #

密码哈希 #

rust
use argon2::{
    password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
    Argon2,
};

pub fn hash_password(password: &str) -> Result<String, String> {
    let salt = SaltString::generate(&mut OsRng);
    Argon2::default()
        .hash_password(password.as_bytes(), &salt)
        .map(|h| h.to_string())
        .map_err(|e| e.to_string())
}

密码强度验证 #

rust
fn validate_password_strength(password: &str) -> Result<(), String> {
    if password.len() < 8 {
        return Err("Password must be at least 8 characters".to_string());
    }
    if !password.chars().any(|c| c.is_uppercase()) {
        return Err("Password must contain uppercase letter".to_string());
    }
    if !password.chars().any(|c| c.is_lowercase()) {
        return Err("Password must contain lowercase letter".to_string());
    }
    if !password.chars().any(|c| c.is_numeric()) {
        return Err("Password must contain number".to_string());
    }
    Ok(())
}

会话安全 #

安全Cookie配置 #

rust
use rocket::http::{Cookie, SameSite};

let cookie = Cookie::build(("session_id", session_id))
    .http_only(true)
    .secure(true)
    .same_site(SameSite::Strict)
    .path("/")
    .max_age(rocket::time::Duration::hours(1))
    .finish();

会话过期 #

rust
use std::time::{Duration, Instant};

struct Session {
    id: String,
    user_id: u32,
    created_at: Instant,
    expires_in: Duration,
}

impl Session {
    fn is_expired(&self) -> bool {
        self.created_at.elapsed() > self.expires_in
    }
}

错误处理 #

不泄露敏感信息 #

rust
#[get("/user/<id>")]
async fn get_user(id: i32, db: &State<Db>) -> Result<Json<User>, Status> {
    db.find_user(id)
        .await
        .map(Json)
        .map_err(|_| Status::NotFound)
}

统一错误响应 #

rust
#[derive(Serialize)]
struct ErrorResponse {
    error: String,
    code: u16,
}

#[catch(404)]
fn not_found() -> Json<ErrorResponse> {
    Json(ErrorResponse {
        error: "Resource not found".to_string(),
        code: 404,
    })
}

#[catch(500)]
fn internal_error() -> Json<ErrorResponse> {
    Json(ErrorResponse {
        error: "Internal server error".to_string(),
        code: 500,
    })
}

速率限制 #

简单速率限制 #

rust
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};

pub struct RateLimiter {
    requests: Mutex<HashMap<String, Vec<Instant>>>,
    max_requests: usize,
    window: Duration,
}

impl RateLimiter {
    pub fn new(max_requests: usize, window_secs: u64) -> Self {
        Self {
            requests: Mutex::new(HashMap::new()),
            max_requests,
            window: Duration::from_secs(window_secs),
        }
    }
    
    pub fn check(&self, key: &str) -> bool {
        let mut requests = self.requests.lock().unwrap();
        let now = Instant::now();
        
        let entry = requests.entry(key.to_string()).or_default();
        entry.retain(|&t| now.duration_since(t) < self.window);
        
        if entry.len() >= self.max_requests {
            false
        } else {
            entry.push(now);
            true
        }
    }
}

安全配置 #

环境变量 #

rust
use std::env;

pub struct Config {
    pub database_url: String,
    pub jwt_secret: String,
    pub is_production: bool,
}

impl Config {
    pub fn from_env() -> Self {
        Self {
            database_url: env::var("DATABASE_URL")
                .expect("DATABASE_URL must be set"),
            jwt_secret: env::var("JWT_SECRET")
                .expect("JWT_SECRET must be set"),
            is_production: env::var("ROCKET_ENV")
                .map(|e| e == "production")
                .unwrap_or(false),
        }
    }
}

Rocket.toml安全配置 #

toml
[default]
secret_key = "your-secret-key-here"
limits = { forms = "64 kB", json = "1 MiB" }

[release]
address = "0.0.0.0"
port = 80
log_level = "critical"

安全检查清单 #

开发阶段 #

  • [ ] 所有输入都经过验证
  • [ ] 使用参数化查询
  • [ ] 密码正确哈希存储
  • [ ] 敏感信息不记录日志

部署阶段 #

  • [ ] 启用HTTPS
  • [ ] 设置安全头部
  • [ ] 配置CORS策略
  • [ ] 启用速率限制

运维阶段 #

  • [ ] 定期更新依赖
  • [ ] 监控异常请求
  • [ ] 定期安全审计
  • [ ] 备份重要数据

完整示例 #

rust
#[macro_use] extern crate rocket;

use rocket::http::{Cookie, CookieJar, SameSite, Status, Header};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::serde::json::Json;
use rocket::serde::{Serialize, Deserialize};
use rocket::form::Form;

struct SecurityHeaders;

#[rocket::async_trait]
impl Fairing for SecurityHeaders {
    fn info(&self) -> Info {
        Info { name: "Security Headers", kind: Kind::Response }
    }

    async fn on_response<'r>(&self, _req: &'r rocket::Request<'_>, res: &mut rocket::Response<'r>) {
        res.set_header(Header::new("X-Content-Type-Options", "nosniff"));
        res.set_header(Header::new("X-Frame-Options", "DENY"));
    }
}

#[derive(FromForm)]
struct LoginForm {
    username: String,
    password: String,
}

#[derive(Serialize)]
struct User {
    id: u32,
    username: String,
}

#[post("/login", data = "<form>")]
fn login(form: Form<LoginForm>, jar: &CookieJar<'_>) -> Result<Json<User>, Status> {
    if form.username == "admin" && form.password == "password" {
        jar.add(
            Cookie::build(("session", "valid-session"))
                .http_only(true)
                .secure(true)
                .same_site(SameSite::Strict)
                .finish()
        );
        Ok(Json(User { id: 1, username: form.username.clone() }))
    } else {
        Err(Status::Unauthorized)
    }
}

#[get("/profile")]
fn profile(jar: &CookieJar<'_>) -> Result<Json<User>, Status> {
    match jar.get("session") {
        Some(c) if c.value() == "valid-session" => {
            Ok(Json(User { id: 1, username: "admin".to_string() }))
        }
        _ => Err(Status::Unauthorized),
    }
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(SecurityHeaders)
        .mount("/api", routes![login, profile])
}

下一步 #

掌握了安全最佳实践后,让我们继续学习 错误处理,了解如何优雅地处理应用错误。

最后更新:2026-03-28