• Github 中文镜像
Sign inSign up
Watch966
Star102.4k
Fork61.8k
Tag: rust
Switch branches/tags
K / 用 Rust Warp 创建一个简单的身份验证服务器.md
移动浏览 Clone
加载中...
到移动设备上浏览
173 lines 17.18 KB
First commit on 22 Aug 2020

    翻译自:Let’s make a simple authentication server in Rust with Warp

    转载请注明出处:http://www.telihai.com/archives/9167/

    首先,使用 cargo 创建一个新项目。

    cargo new warp_auth_server
    cd warp_auth_server
    

    然后将 warp 依赖项添加到 Cargo.toml

    [dependencies]
    warp = "0.2.0"
    

    使用 async Rust 时,我们还需要使用执行程序来轮询 Futures,因此我们添加依赖项 tokio 以完成此任务。 tokio 已由 warp 内部使用,但我们仍需要在项目中明确包含它。

    [dependencies]
    warp = "0.2.0"
    tokio = { version = "0.2", features = ["macros"] }
    

    然后编辑 src/main.rs 并用 warp hello world 替换 hello world。

    use warp::Filter;
    
    #[tokio::main]
    async fn main() {
        let routes = warp::any().map(|| "Hello, World!");
        warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
    }
    

    然后你可以用 cargo run 启动服务并且在浏览器中访问 127.0.0.1:3030 看到 “Hello, World!”。

    在 hello world 程序中,已经向您介绍了 Filters,它是 warp 中的主要概念。每个传入的请求都通过可以处理 request 或 reject 它的 Filters 链。从根本上讲,这是非常简单的,但仍然足够强大,可以实现复杂的路由和中间件之类的功能,我们将在后面进行探讨。在 hello world 示例中,warp::any() 是接受任何请求的 Filter

    让我们看看如何使用 warp Filters 处理我们项目的路由。在 hello world 示例中用三个不同的路径替换 routes

    let register = warp::path("register").map(|| "Hello from register");
    let login = warp::path("login").map(|| "Hello from login");
    let logout = warp::path("logout").map(|| "Hello from logout");
    let routes = register.or(login).or(logout);
    

    注意,不是使用 warp::any() Filter,而是使用 warp::path(),它将接受对给定字符串匹配的路径的任何请求,以及 reject 任何其他请求。为了合并所有路由,我们使用了 oror 会在 Filter 拒绝后尝试的另一条链,因此,在我们的示例中,被拒绝的任何请求 "register" Filter 都将沿着下一条 Filter 链(即 "login" 路由)发送。这将一直进行到 Filter 链条之一产生响应为止,否则将发送错误响应。以及or,还有 and 一个用于在不拒绝请求时将过滤器链接在一起。我们可以通过将所有路径放在 "/api" 路径后进行测试。

    let routes = register.or(login).or(logout);
    let routes = warp::path("api").and(routes);
    

    这里,如果请求被接受 "api" Filter,即任何到 "/api/*"and 所定义路径中的请求,都会 yield 一个响应。

    在继续之前要掌握的最后一个概念是 Filters 可用于在整个项目中共享状态。我们将看一个共享计数器的简单示例。

    use std::sync::Arc;
    use tokio::sync::Mutex;
    use warp::Filter;
    
    #[tokio::main]
    async fn main() {
        let db = Arc::new(Mutex::new(0));
        let db = warp::any().map(move || Arc::clone(&db));
        let routes = warp::path("counter").and(db.clone()).and_then(counter);
        warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
    }
    
    async fn counter(db: Arc<Mutex<u8>>) -> Result<impl warp::Reply, warp::Rejection> {
        let mut counter = db.lock().await;
        *counter += 1;
        Ok(counter.to_string())
    }
    

    这里要注意的是,我们在 main 中定义了一个 0 的初始状态,用 tokio MutexArc 包裹以便可以异步共享和改变它。之后,我们将计数器添加到 Filter 以便我们可以将其与其他计数器结合使用。在此示例中,我们的路由接受带有 "counter" 路径的任何请求,然后将 db 添加到接下来 Filters 使用的 request 中,最后将其传递给函数。注意,最后 and 被替换为与 async 函数一起使用的 and_then

    实现认证服务器

    有了足够的基础知识,现在我们可以继续实施身份验证服务器。首先,我们需要用用户数据库代替计数器。为此,我们将仅使用内存数据库,但以后可以轻松替换它。

    let db = Arc::new(Mutex::new(HashMap::<String, User>::new()));
    let db = warp::any().map(move || Arc::clone(&db));
    

    记住要从标准库 include HashMap

    use std::collections::HashMap;
    

    在定义 User 结构之前,我们应该添加一个名为 serde 的依赖项,以便我们可以从请求中反序列化 JSON 数据。

    [dependencies]
    warp = "0.2.0"
    tokio = { version = "0.2", features = ["macros"] }
    serde = { version = "1.0", features = ["derive"] }
    

    然后使用 serde::Deserializemain.rs 并定义 User 结构。

    use serde::Deserialize;
    
    #[derive(Deserialize)]
    struct User {
        username: String,
        password: String,
    }
    

    接下来,我们可以制作将在每个 Filter 链的末尾调用的函数。在这些文件中,我们将使用 warp 导出的 HTTP 状态代码作为响应,因此我们需要将其包含在 main.rs 中。

    use warp::http::StatusCode;
    

    现在让我们做一个注册用户的功能。

    async fn register(
        new_user: User,
        db: Arc<Mutex<HashMap<String, User>>>,
    ) -> Result<impl warp::Reply, warp::Rejection> {
        let mut users = db.lock().await;
        if users.contains_key(&new_user.username) {
            return Ok(StatusCode::BAD_REQUEST);
        }
        users.insert(new_user.username.clone(), new_user);
        Ok(StatusCode::CREATED)
    }
    

    此功能需要一个新用户,如果该用户名不存在,则用户的数据库会将新用户添加到数据库中。请注意,这会将密码存储为纯文本格式,您绝对不要这样做,我们将在以后进行修复。

    现在,处理登录的功能。

    async fn login(
        credentials: User,
        db: Arc<Mutex<HashMap<String, User>>>,
    ) -> Result<impl warp::Reply, warp::Rejection> {
        let users = db.lock().await;
        match users.get(&credentials.username) {
            None => Ok(StatusCode::BAD_REQUEST),
            Some(user) => {
                if credentials.password == user.password {
                    Ok(StatusCode::OK)
                } else {
                    Ok(StatusCode::UNAUTHORIZED)
                }
            }
        }
    }
    

    此功能采用给定的凭据,并检查是否存在具有这些凭据的用户。我们的示例仅在成功时返回 200 OK 响应,但是您可以返回 Cookie 或 JWT 之类的内容,并在此处处理会话。

    最后,让我们定义路由。

    let register = warp::post()
        .and(warp::path("register"))
        .and(warp::body::json())
        .and(db.clone())
        .and_then(register);
    let login = warp::post()
        .and(warp::path("login"))
        .and(warp::body::json())
        .and(db.clone())
        .and_then(login);
    
    let routes = register.or(login);
    warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
    

    您现在可能已经猜到了 warp::body::json() Filter 做了什么。像我们的数据库 Filter 一样,它将 request body 添加到我们的请求中,以供Filter 链的其余部分使用。我们还使用 warp::post() Filter 在此处拒绝任何不是 HTTP POST 请求的内容。

    现在,您可以运行服务器并使用 curl 或 HTTPie 之类的工具对其进行测试。

    存储密码哈希

    还记得我们是如何将密码以纯文本格式存储在数据库中的吗?这是不好的做法,相反,我们应该存储密码的哈希值,所以现在就实现它。我们将需要添加一些哈希密码依赖项。

    [dependencies]
    warp = "0.2.0"
    tokio = { version = "0.2", features = ["macros"] }
    serde = { version = "1.0", features = ["derive"] }
    rand = "0.7.2"
    rust-argon2 = "0.6.0"
    

    为了方便起见,我们将使用一些包装函数来哈希和验证密码。

    use argon2::{self, Config};
    use rand::Rng;
    
    pub fn hash(password: &[u8]) -> String {
        let salt = rand::thread_rng().gen::<[u8; 32]>();
        let config = Config::default();
        argon2::hash_encoded(password, &salt, &config).unwrap()
    }
    
    pub fn verify(hash: &str, password: &[u8]) -> bool {
        argon2::verify_encoded(hash, password).unwrap_or(false)
    }
    

    这里要注意的最重要的一点是,我们正在 hash 函数中生成随机盐。最佳做法是为每个密码生成随机盐,因为它可以防止攻击者可能使用的各种攻击。

    现在,我们需要更换 insert 我们的 register 功能。

    let hashed_user = User {
        username: new_user.username,
        password: hash(new_user.password.as_bytes()),
    };
    users.insert(hashed_user.username.clone(), hashed_user);
    

    还有 login 函数中的 if

    if verify(&user.password, credentials.password.as_bytes()) {
        Ok(StatusCode::OK)
    } else {
        Ok(StatusCode::UNAUTHORIZED)
    }
    

    好多了。目前为止就这样了。这是一个非常简单的身份验证服务器,但我希望本文能为您提供扩展它以满足您自己需要的构建块。我强烈建议您查看 warp 文档,如果需要帮助,请随时询问我。另外,欢迎任何反馈!

    我已经将最终验证服务器的完整代码放在 GitHub 上。