CSRF防护 #
CSRF(跨站请求伪造)是一种常见的Web安全漏洞。本节将介绍如何在Rocket中实现CSRF防护。
CSRF攻击原理 #
text
1. 用户登录正常网站A
2. 用户访问恶意网站B
3. 网站B向网站A发送请求
4. 请求携带用户在A的Cookie
5. 网站A误认为是用户操作
防护策略 #
1. SameSite Cookie #
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