CSRF防护 #

CSRF(跨站请求伪造)是一种常见的Web安全漏洞。本节将介绍如何在Rocket中实现CSRF防护。

CSRF攻击原理 #

text
1. 用户登录正常网站A
2. 用户访问恶意网站B
3. 网站B向网站A发送请求
4. 请求携带用户在A的Cookie
5. 网站A误认为是用户操作

防护策略 #

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

#[get("/set-cookie")]
fn set_secure_cookie(jar: &CookieJar<'_>) -> &'static str {
    jar.add(
        Cookie::build(("session_id", "abc123"))
            .same_site(SameSite::Strict)
            .http_only(true)
            .secure(true)
            .finish()
    );
    "Cookie set"
}
SameSite值 说明
Strict 完全禁止跨站发送Cookie
Lax 允许GET请求跨站发送
None 允许跨站发送(需配合Secure)

2. CSRF Token #

Token生成 #

rust
use rand::Rng;

pub fn generate_csrf_token() -> String {
    let mut rng = rand::thread_rng();
    let token: [u8; 32] = rng.gen();
    base64::encode(&token)
}

Token验证中间件 #

rust
use rocket::http::Status;
use rocket::request::{self, FromRequest, Request, Outcome};

pub struct CsrfToken(pub String);

#[rocket::async_trait]
impl<'r> FromRequest<'r> for CsrfToken {
    type Error = CsrfError;

    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let cookie_token = request.cookies()
            .get("csrf_token")
            .map(|c| c.value().to_string());
        
        let header_token = request.headers()
            .get_one("X-CSRF-Token")
            .map(|s| s.to_string());
        
        match (cookie_token, header_token) {
            (Some(cookie), Some(header)) if cookie == header => {
                Outcome::Success(CsrfToken(cookie))
            }
            _ => Outcome::Error((Status::Forbidden, CsrfError::InvalidToken)),
        }
    }
}

#[derive(Debug)]
pub enum CsrfError {
    InvalidToken,
}

使用CSRF Token #

rust
#[post("/transfer", format = "json", data = "<data>")]
fn transfer(csrf: CsrfToken, data: Json<TransferRequest>) -> String {
    format!("Transfer approved with CSRF token: {}", csrf.0)
}

3. 双重提交Cookie #

rust
use rocket::http::{Cookie, SameSite};
use rocket::request::{self, FromRequest, Request, Outcome};
use rocket::http::Status;

pub struct CsrfProtected;

#[rocket::async_trait]
impl<'r> FromRequest<'r> for CsrfProtected {
    type Error = ();

    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let cookie_token = request.cookies()
            .get("XSRF-TOKEN")
            .map(|c| c.value());
        
        let header_token = request.headers()
            .get_one("X-XSRF-TOKEN");
        
        match (cookie_token, header_token) {
            (Some(cookie), Some(header)) if cookie == header => {
                Outcome::Success(CsrfProtected)
            }
            _ => Outcome::Error((Status::Forbidden, ())),
        }
    }
}

完整CSRF防护实现 #

CSRF Fairing #

rust
use rocket::{Request, Data, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::{Cookie, SameSite, Header};
use rand::Rng;

pub struct CsrfFairing;

#[rocket::async_trait]
impl Fairing for CsrfFairing {
    fn info(&self) -> Info {
        Info {
            name: "CSRF Protection",
            kind: Kind::Request | Kind::Response,
        }
    }

    async fn on_request(&self, request: &mut Request<'_>, _data: &mut Data<'_>) {
        if request.method() != rocket::http::Method::Get {
            let cookie_token = request.cookies()
                .get("csrf_token")
                .map(|c| c.value().to_string());
            
            let header_token = request.headers()
                .get_one("X-CSRF-Token")
                .map(|s| s.to_string());
            
            if cookie_token != header_token {
                request.local_cache(|| false);
            }
        }
    }

    async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
        if !request.cookies().get("csrf_token").is_some() {
            let token = generate_token();
            response.set_header(Header::new("X-CSRF-Token", token.clone()));
        }
    }
}

fn generate_token() -> String {
    let mut rng = rand::thread_rng();
    let token: [u8; 32] = rng.gen();
    base64::encode(&token)
}

前端集成 #

html
<script>
// 从Cookie获取CSRF Token
function getCsrfToken() {
    const match = document.cookie.match(/csrf_token=([^;]+)/);
    return match ? match[1] : null;
}

// 发送请求时带上Token
async function fetchWithCsrf(url, options = {}) {
    const token = getCsrfToken();
    
    return fetch(url, {
        ...options,
        headers: {
            ...options.headers,
            'X-CSRF-Token': token
        }
    });
}

// 使用示例
fetchWithCsrf('/api/transfer', {
    method: 'POST',
    body: JSON.stringify({ amount: 100 })
});
</script>

表单CSRF防护 #

模板中嵌入Token #

rust
use rocket_dyn_templates::Template;
use rocket::serde::Serialize;

#[derive(Serialize)]
struct FormContext {
    csrf_token: String,
}

#[get("/form")]
fn form() -> Template {
    let token = generate_csrf_token();
    Template::render("form", &FormContext { csrf_token: token })
}

模板文件 #

html
<form action="/submit" method="POST">
    <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
    <input type="text" name="data">
    <button type="submit">Submit</button>
</form>

表单验证 #

rust
use rocket::form::Form;

#[derive(FromForm)]
struct FormData {
    csrf_token: String,
    data: String,
}

#[post("/submit", data = "<form>")]
fn submit(form: Form<FormData>, jar: &CookieJar<'_>) -> Result<String, Status> {
    let session_token = jar.get("csrf_token")
        .map(|c| c.value())
        .unwrap_or("");
    
    if form.csrf_token != session_token {
        return Err(Status::Forbidden);
    }
    
    Ok(format!("Submitted: {}", form.data))
}

最佳实践 #

1. 所有状态修改操作都需要CSRF防护 #

rust
#[post("/api/user/delete")]
fn delete_user(csrf: CsrfToken) -> String {
    "User deleted"
}

2. GET请求不应修改状态 #

rust
#[get("/user/delete")]
fn bad_delete_user() -> &'static str {
    "Don't do this!"
}

#[post("/user/delete")]
fn good_delete_user(csrf: CsrfToken) -> &'static str {
    "User deleted"
}

3. 使用SameSite=Strict或Lax #

rust
Cookie::build(("session", "value"))
    .same_site(SameSite::Strict)
    .finish()

完整示例 #

rust
#[macro_use] extern crate rocket;

use rocket::http::{Cookie, CookieJar, SameSite, Status};
use rocket::form::Form;
use rocket::response::content::RawHtml;

fn generate_token() -> String {
    use rand::Rng;
    let mut rng = rand::thread_rng();
    let token: [u8; 16] = rng.gen();
    hex::encode(token)
}

#[derive(FromForm)]
struct TransferForm {
    csrf_token: String,
    amount: f64,
    to: String,
}

#[get("/")]
fn index(jar: &CookieJar<'_>) -> RawHtml<String> {
    let token = jar.get("csrf_token")
        .map(|c| c.value().to_string())
        .unwrap_or_else(|| {
            let t = generate_token();
            jar.add(
                Cookie::build(("csrf_token", t.clone()))
                    .same_site(SameSite::Strict)
                    .http_only(false)
                    .finish()
            );
            t
        });
    
    RawHtml(format!(r#"
        <form action="/transfer" method="POST">
            <input type="hidden" name="csrf_token" value="{}">
            <input type="text" name="to" placeholder="Recipient">
            <input type="number" name="amount" placeholder="Amount">
            <button type="submit">Transfer</button>
        </form>
    "#, token))
}

#[post("/transfer", data = "<form>")]
fn transfer(form: Form<TransferForm>, jar: &CookieJar<'_>) -> Result<String, Status> {
    let session_token = jar.get("csrf_token")
        .map(|c| c.value())
        .unwrap_or("");
    
    if form.csrf_token != session_token {
        return Err(Status::Forbidden);
    }
    
    Ok(format!("Transferred ${:.2} to {}", form.amount, form.to))
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index, transfer])
}

下一步 #

掌握了CSRF防护后,让我们继续学习 安全最佳实践,了解更多Web安全知识。

最后更新:2026-03-28