翻译自: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 时,我们还需要使用执行程序来轮询 Future
s,因此我们添加依赖项 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 程序中,已经向您介绍了 Filter
s,它是 warp
中的主要概念。每个传入的请求都通过可以处理 request 或 reject
它的 Filter
s 链。从根本上讲,这是非常简单的,但仍然足够强大,可以实现复杂的路由和中间件之类的功能,我们将在后面进行探讨。在 hello world 示例中,warp::any()
是接受任何请求的 Filter
。
让我们看看如何使用 warp
Filter
s 处理我们项目的路由。在 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
任何其他请求。为了合并所有路由,我们使用了 or
。 or
会在 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 一个响应。
在继续之前要掌握的最后一个概念是 Filter
s 可用于在整个项目中共享状态。我们将看一个共享计数器的简单示例。
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
Mutex
和 Arc
包裹以便可以异步共享和改变它。之后,我们将计数器添加到 Filter
以便我们可以将其与其他计数器结合使用。在此示例中,我们的路由接受带有 "counter"
路径的任何请求,然后将 db
添加到接下来 Filter
s 使用的 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::Deserialize
中 main.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 上。