身份验证 #

身份验证是Web应用安全的核心。本节将介绍如何在Rocket中实现安全的用户认证系统。

密码哈希 #

使用argon2 #

toml
[dependencies]
argon2 = "0.5"
rand = "0.8"

密码哈希实现 #

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

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

pub fn verify_password(password: &str, hash: &str) -> bool {
    let parsed_hash = match PasswordHash::new(hash) {
        Ok(h) => h,
        Err(_) => return false,
    };
    
    Argon2::default()
        .verify_password(password.as_bytes(), &parsed_hash)
        .is_ok()
}

JWT认证 #

添加依赖 #

toml
[dependencies]
jsonwebtoken = "9.2"
serde = { version = "1.0", features = ["derive"] }

JWT配置 #

rust
use serde::{Serialize, Deserialize};
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey};

#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    pub sub: String,
    pub username: String,
    pub exp: usize,
    pub iat: usize,
}

pub struct JwtConfig {
    pub secret: String,
    pub expiration: u64,
}

impl JwtConfig {
    pub fn new(secret: String, expiration_hours: u64) -> Self {
        Self {
            secret,
            expiration: expiration_hours * 3600,
        }
    }
    
    pub fn generate_token(&self, user_id: u32, username: &str) -> Result<String, String> {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs() as usize;
        
        let claims = Claims {
            sub: user_id.to_string(),
            username: username.to_string(),
            exp: now + self.expiration as usize,
            iat: now,
        };
        
        encode(
            &Header::default(),
            &claims,
            &EncodingKey::from_secret(self.secret.as_bytes()),
        ).map_err(|e| e.to_string())
    }
    
    pub fn validate_token(&self, token: &str) -> Result<Claims, String> {
        decode::<Claims>(
            token,
            &DecodingKey::from_secret(self.secret.as_bytes()),
            &Validation::new(Algorithm::HS256),
        )
        .map(|data| data.claims)
        .map_err(|e| e.to_string())
    }
}

JWT守卫 #

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

pub struct AuthUser {
    pub user_id: u32,
    pub username: String,
}

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

    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let config = request.guard::<&rocket::State<JwtConfig>>()
            .await
            .unwrap();
        
        let auth_header = request.headers()
            .get_one("Authorization")
            .and_then(|h| h.strip_prefix("Bearer "));
        
        match auth_header {
            Some(token) => {
                match config.validate_token(token) {
                    Ok(claims) => Outcome::Success(AuthUser {
                        user_id: claims.sub.parse().unwrap_or(0),
                        username: claims.username,
                    }),
                    Err(_) => Outcome::Error((Status::Unauthorized, AuthError::InvalidToken)),
                }
            }
            None => Outcome::Error((Status::Unauthorized, AuthError::MissingToken)),
        }
    }
}

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

使用JWT认证 #

rust
#[post("/login", format = "json", data = "<credentials>")]
async fn login(
    credentials: Json<LoginRequest>,
    config: &State<JwtConfig>,
    db: &State<Db>,
) -> Result<Json<LoginResponse>, Status> {
    let user = db.find_user_by_email(&credentials.email)
        .await
        .map_err(|_| Status::Unauthorized)?;
    
    if !verify_password(&credentials.password, &user.password_hash) {
        return Err(Status::Unauthorized);
    }
    
    let token = config.generate_token(user.id, &user.username)
        .map_err(|_| Status::InternalServerError)?;
    
    Ok(Json(LoginResponse {
        token,
        user: UserInfo {
            id: user.id,
            username: user.username,
            email: user.email,
        },
    }))
}

#[get("/profile")]
fn profile(user: AuthUser) -> Json<UserInfo> {
    Json(UserInfo {
        id: user.user_id,
        username: user.username,
        email: "user@example.com".to_string(),
    })
}

OAuth集成 #

OAuth流程 #

text
1. 用户点击登录 → 重定向到OAuth提供商
2. 用户授权 → 回调到应用
3. 获取access_token → 获取用户信息
4. 创建会话 → 返回应用

GitHub OAuth示例 #

rust
use rocket::response::Redirect;
use serde::Deserialize;

#[derive(Deserialize)]
struct GitHubToken {
    access_token: String,
}

#[derive(Deserialize)]
struct GitHubUser {
    login: String,
    id: u64,
    email: Option<String>,
}

#[get("/auth/github")]
fn github_login() -> Redirect {
    let client_id = "your-github-client-id";
    let redirect_uri = "http://localhost:8000/auth/github/callback";
    
    let url = format!(
        "https://github.com/login/oauth/authorize?client_id={}&redirect_uri={}&scope=user:email",
        client_id, redirect_uri
    );
    
    Redirect::to(url)
}

#[get("/auth/github/callback?<code>")]
async fn github_callback(code: &str) -> Result<Redirect, Status> {
    let client = reqwest::Client::new();
    
    let token_response = client
        .post("https://github.com/login/oauth/access_token")
        .query(&[
            ("client_id", "your-github-client-id"),
            ("client_secret", "your-github-client-secret"),
            ("code", code),
        ])
        .header("Accept", "application/json")
        .send()
        .await
        .map_err(|_| Status::InternalServerError)?
        .json::<GitHubToken>()
        .await
        .map_err(|_| Status::InternalServerError)?;
    
    let user = client
        .get("https://api.github.com/user")
        .header("Authorization", format!("token {}", token_response.access_token))
        .header("User-Agent", "Rocket-App")
        .send()
        .await
        .map_err(|_| Status::InternalServerError)?
        .json::<GitHubUser>()
        .await
        .map_err(|_| Status::InternalServerError)?;
    
    println!("GitHub user: {:?}", user);
    
    Ok(Redirect::to("/"))
}

完整示例 #

rust
#[macro_use] extern crate rocket;

use rocket::serde::json::Json;
use rocket::serde::{Serialize, Deserialize};
use rocket::State;
use rocket::http::Status;
use rocket::request::{self, FromRequest, Request, Outcome};

#[derive(Serialize, Deserialize)]
struct Claims {
    sub: String,
    username: String,
    exp: usize,
}

struct JwtConfig {
    secret: String,
}

impl JwtConfig {
    fn generate(&self, user_id: u32, username: &str) -> String {
        let exp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs() as usize + 3600;
        
        let claims = Claims {
            sub: user_id.to_string(),
            username: username.to_string(),
            exp,
        };
        
        jsonwebtoken::encode(
            &jsonwebtoken::Header::default(),
            &claims,
            &jsonwebtoken::EncodingKey::from_secret(self.secret.as_bytes()),
        ).unwrap()
    }
    
    fn validate(&self, token: &str) -> Option<Claims> {
        jsonwebtoken::decode::<Claims>(
            token,
            &jsonwebtoken::DecodingKey::from_secret(self.secret.as_bytes()),
            &jsonwebtoken::Validation::default(),
        ).ok().map(|d| d.claims)
    }
}

struct AuthUser {
    user_id: u32,
    username: String,
}

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

    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let config = request.guard::<&State<JwtConfig>>().await.unwrap();
        
        let token = request.headers()
            .get_one("Authorization")
            .and_then(|h| h.strip_prefix("Bearer "));
        
        match token.and_then(|t| config.validate(t)) {
            Some(claims) => Outcome::Success(AuthUser {
                user_id: claims.sub.parse().unwrap(),
                username: claims.username,
            }),
            None => Outcome::Error((Status::Unauthorized, ())),
        }
    }
}

#[derive(Deserialize)]
struct LoginRequest {
    username: String,
    password: String,
}

#[derive(Serialize)]
struct LoginResponse {
    token: String,
}

#[post("/login", format = "json", data = "<req>")]
fn login(req: Json<LoginRequest>, config: &State<JwtConfig>) -> Json<LoginResponse> {
    if req.username == "admin" && req.password == "password" {
        let token = config.generate(1, &req.username);
        Json(LoginResponse { token })
    } else {
        Json(LoginResponse { token: String::new() })
    }
}

#[get("/profile")]
fn profile(user: AuthUser) -> String {
    format!("Welcome, {} (ID: {})", user.username, user.user_id)
}

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

下一步 #

掌握了身份验证后,让我们继续学习 CSRF防护,了解如何防止跨站请求伪造攻击。

最后更新:2026-03-28