idea火箭_火箭认证

idea火箭

Last week we enhanced our Rocket web server. We combined our server with our Diesel schema to enable a series of basic CRUD endpoints. This week, we’ll continue this integration, but bring in some more cool Rocket features. We’ll explore two different methods of authentication. First, we’ll create a “Request Guard” to allow a form of Basic Authentication. Then we’ll also explore Rocket’s amazingly simple Cookies integration.

上周,我们增强了Rocket Web服务器。 我们将服务器与Diesel模式结合在一起,以启用一系列基本的CRUD端点。 本周,我们将继续进行这种集成,但会引入一些更酷的Rocket功能。 我们将探讨两种不同的身份验证方法。 首先,我们将创建一个“请求防护”以允许某种形式的基本身份验证。 然后,我们还将探讨Rocket惊人的简单Cookie集成。

As always, you can explore the code for this series by heading to our Github repository. For this article specifically, you’ll want to take a look at the rocket_auth.rs file

与往常一样,您可以前往我们的Github存储库探索本系列的代码。 具体来说,对于本文,您将要看一下rocket_auth.rs 文件

If you’re just starting your Rust journey, feel free to check out our Beginners Series as well!

如果您刚刚开始Rust之旅,请随时查看我们的初学者系列

新数据类型 (New Data Types)

To start off, let’s make a few new types to help us. First, we’ll need a new database table, auth_infos, based on this struct:

首先,让我们创建一些新类型来帮助我们。 首先,我们需要基于此结构的新数据库表auth_infos

#[derive(Insertable)]
pub struct AuthInfo {
pub user_id: i32,
pub password_hash: String
}

When the user creates their account, they’ll provide a password. We’ll store a hash of that password in our database table. Of course, you’ll want to run through all the normal steps we did with Diesel to create this table. This includes having the corresponding Entity type.

用户创建帐户时,将提供密码。 我们将在数据库表中存储该密码的哈希。 当然,您将需要完成与Diesel一起创建此表所执行的所有常规步骤。 这包括具有相应的Entity类型。

We’ll also want a couple new form types to accept authentication information. First off, when we create a user, we’ll now include the password in the form.

我们还希望有两种新的表单类型可以接受身份验证信息。 首先,当我们创建用户时,现在将在表单中包含密码。

#[derive(FromForm, Deserialize)]
struct CreateInfo {
name: String,
email: String,
age: i32,
password: String
}

Second, when a user wants to login, they’ll pass their username (email) and their password.

其次,当用户想要登录时,他们将传递其用户名(电子邮件)和密码。

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

Both these types should derive FromForm and Deserialize so we can grab them out of "post" data. You might wonder, do we need another type to store the same information that already exists in User and UserEntity? It would be possible to write CreateInfo to have a User within it. But then we'd have to manually write the FromForm instance. This isn't difficult, but it might be more tedious than using a new type.

这两种类型都应派生FromFormDeserialize以便我们可以从“发布”数据中获取它们。 您可能想知道,我们是否需要另一种类型来存储UserUserEntity中已经存在的相同信息? 可以编写CreateInfo在其中包含一个User 。 但是然后我们必须手动编写FromForm实例。 这并不困难,但是可能比使用新类型更乏味。

创建一个用户 (Creating a User)

So in the first place, we have to create our user so they’re matched up with their password. This requires taking the CreateInfo in our post request. We'll first unwrap the user fields and insert our User object. This follows the patterns we've seen so far in this series with Diesel.

因此,首先,我们必须创建我们的用户,以使其与密码匹配。 这需要在我们的发布请求中使用CreateInfo 。 我们将首先打开用户字段并插入我们的User对象。 这遵循了到目前为止在Diesel系列中看到的模式。

#[post("/users/create", format="json", data="<create_info>")]
fn create(db: State<String>, create_info: Json<CreateInfo>)
-> Json<i32> {
let user: User = User
{ name: create_info.name.clone(),
email: create_info.email.clone(),
age: create_info.age};
let connection = ...;
let user_entity: UserEntity = diesel::insert_into(users::table)...

}

Now we’ll want a function for hashing our password. We’ll use the SHA3 algorithm, courtesy of the rust-crypto library:

现在,我们需要一个用于哈希密码的函数。 我们将使用rust-crypto库提供的SHA3算法:

fn hash_password(password: &String) -> String {
let mut hasher = Sha3::sha3_256();
hasher.input_str(password);
hasher.result_str()
}

We’ll apply this function on the input password and attach it to the created user ID. Then we can insert the new AuthInfo and return the created ID.

我们将在输入密码上应用此功能,并将其附加到创建的用户ID上。 然后,我们可以插入新的AuthInfo并返回创建的ID。

#[post("/users/create", format="json", data="<create_info>")]
fn create(db: State<String>, create_info: Json<CreateInfo>)
-> Json<i32> {
...
let user_entity: UserEntity = diesel::insert_into(users::table)... let password_hash = hash_password(&create_info.password);
let auth_info: AuthInfo = AuthInfo
{user_id: user_entity.id, password_hash: password_hash};
let auth_info_entity: AuthInfoEntity =
diesel::insert_into(auth_infos::table)..
Json(user_entity.id)
}

Now whenever we create our user, they’ll have their password attached!

现在,无论何时创建用户,他们都将附加密码!

门控端点 (Gating an Endpoint)

Now that our user has a password, how do we gate endpoints on authentication? Well the first approach we can try is something like “Basic Authentication”. This means that every authenticated request contains the username and the password. In our example we’ll get these directly out of header elements. But in a real application you would want to double check that the request is encrypted before doing this.

现在,我们的用户有了密码,我们如何在身份验证时控制端点? 好吧,我们可以尝试的第一种方法是“基本身份验证”。 这意味着每个经过身份验证的请求都包含用户名和密码。 在我们的示例中,我们将直接从标题元素中获取这些内容。 但是在实际的应用程序中,您需要在执行此操作之前仔细检查请求是否已加密。

But it would be tiresome to apply the logic of reading the headers in every handler. So Rocket has a powerful functionality called “Request Guards”. Rocket has a special trait called FromRequest. Whenever a particular type is an input to a handler function, it runs the from_request function. This determines how to derive the value from the request. In our case, we'll make a wrapper type AuthenticatedUser. This represents a user that has included their auth info in the request.

但是在每个处理程序中应用读取标头的逻辑将很麻烦。 因此,Rocket具有强大的功能,称为“请求防护”。 火箭有一个特殊的特性,叫做FromRequest 。 只要特定类型是处理程序函数的输入,它就会运行from_request函数。 这确定了如何从请求中导出值。 在我们的例子中,我们将创建一个包装类型AuthenticatedUser 。 这表示已在请求中包含其身份验证信息的用户。

struct AuthenticatedUser {
user_id: i32
}

Now we can include this type in a handler signature. For this endpoint, we only allow a user to retrieve their data if they’ve logged in:

现在,我们可以在处理程序签名中包含此类型。 对于此端点,我们仅允许用户在登录后检索其数据:

#[get("/users/my_data")]
fn login(db: State<String>, user: AuthenticatedUser)
-> Json<Option<UserEntity>> {
Json(fetch_user_by_id(&db, user.user_id))
}

实施请求特征 (Implementing the Request Trait)

The trick of course is that we need to implement the FromRequest trait! This is more complicated than it sounds! Our handler will have the ability to short-circuit the request and return an error. So let's start by specifying a couple potential login errors we can throw.

当然,诀窍是我们需要实现FromRequest特性! 这比听起来要复杂! 我们的处理程序将能够使请求短路并返回错误。 因此,让我们从指定一些可能引发的潜在登录错误开始。

#[derive(Debug)]
enum LoginError {
InvalidData,
UsernameDoesNotExist,
WrongPassword
}

The from_request function will take in a request and return an Outcome. The outcome will either provide our authentication type or an error. The last bit of adornment we need on this is lifetime specifiers for the request itself and the reference to it.

from_request函数将接受请求并返回Outcome 。 结果将提供我们的身份验证类型或错误。 我们需要做的最后一点装饰是请求本身及其引用的生命周期说明符。

impl<'a, 'r> FromRequest<'a, 'r> for AuthenticatedUser {
type Error = LoginError;
fn from_request(request: &'a Request<'r>)
-> Outcome<AuthenticatedUser, LoginError> {
...
}
}

Now the actual function definition involves several layers of case matching! It consists of a few different operations that have to query the request or query our database. For example, let’s consider the first layer. We insist on having two headers in our request: one for the username, and one for the password. We’ll use request.headers() to check for these values. If either doesn't exist, we'll send a Failure outcome with invalid data. Here's what that looks like:

现在,实际的函数定义涉及几层大小写匹配! 它由一些不同的操作组成,这些操作必须查询请求或查询我们的数据库。 例如,让我们考虑第一层。 我们坚持在请求中包含两个标题:一个用于用户名,另一个用于密码。 我们将使用request.headers()检查这些值。 如果不存在,我们将发送包含无效数据的Failure结果。 看起来像这样:

impl<'a, 'r> FromRequest<'a, 'r> for AuthenticatedUser {
type Error = LoginError;
fn from_request(request: &'a Request<'r>)
-> Outcome<AuthenticatedUser, LoginError> {
let username = request.headers().get_one("username");
let password = request.headers().get_one("password");
match (username, password) {
(Some(u), Some(p)) => {
...
}
_ => Outcome::Failure(
(Status::BadRequest,
LoginError::InvalidData))
}
}
}

In the main branch of the function, we’ll do 3 steps:

在函数的主分支中,我们将执行3个步骤:

  1. Find the user in our database based on their email address/username.

    根据他们的电子邮件地址/用户名在我们的数据库中查找用户。
  2. Find their authentication information based on the ID

    根据ID查找其身份验证信息
  3. Hash the input password and compare it to the database hash

    哈希输入的密码并将其与数据库哈希进行比较

If we are successful, then we’ll return a successful outcome:

如果我们成功,那么我们将返回成功的结果:

Outcome::Success(AuthenticatedUser(user_id: user.id))

The number of match levels required makes the function definition very verbose. So we've included it at the bottom as an appendix. We know how to take such a function and write it more cleanly in Haskell using monads. In a couple weeks, we'll use this function as a case study to explore Rust's monadic abilities.

所需的match级别数使函数定义非常冗长。 因此,我们将其作为附录包含在底部。 我们知道如何使用这样的函数,并使用monad在Haskell中更清晰地编写它。 在几周后,我们将使用此功能作为案例研究来探索Rust的单子函数。

使用Cookie登录 (Logging in with Cookies)

In most applications though, we’ll won’t want to include the password in the request each time. In HTTP, “Cookies” provide a way to store information about a particular user that we can track on our server.

但是,在大多数应用程序中,我们不想每次都在请求中包含密码。 在HTTP中,“ Cookies”提供了一种存储有关特定用户的信息的方式,我们可以在服务器上对其进行跟踪。

Rocket makes this very easy with the Cookies type! We can always include this mutable type in our requests. It works like a key-value store, where we can access certain information with a key like "user_id". Since we're storing auth information, we'll also want to make sure it's encoded, or "private". So we'll use these functions:

火箭让Cookies类型变得非常容易! 我们总是可以在我们的请求中包括这种可变类型。 它的工作方式类似于键值存储,我们可以在其中使用"user_id"类的键来访问某些信息。 由于我们存储的是身份验证信息,因此我们还要确保它已编码或“私有”。 因此,我们将使用以下功能:

add_private(...)
get_private(...)
remove_private(...)

Let’s start with a “login” endpoint. This will take our LoginInfo object as its post data, but we'll also have the Cookies input:

让我们从“登录”端点开始。 这将把我们的LoginInfo对象作为其发布数据,但我们还将获得Cookies输入:

#[post("/users/login", format="json", data="<login_info>")]
fn login_post(db: State<String>, login_info: Json<LoginInfo>, mut cookies: Cookies) -> Json<Option<i32>> {
...
}

First we have to make sure a user of that name exists in the database:

首先,我们必须确保该名称的用户存在于数据库中:

#[post("/users/login", format="json", data="<login_info>")]
fn login_post(
db: State<String>,
login_info: Json<LoginInfo>,
mut cookies: Cookies)
-> Json<Option<i32>> {
let maybe_user = fetch_user_by_email(&db, &login_info.username);
match maybe_user {
Some(user) => {
...
}
}
None => Json(None)
}
}

Then we have to get their auth info again. We’ll hash the password and compare it. If we’re successful, then we’ll add the user’s ID as a cookie. If not, we’ll return None.

然后,我们必须再次获取其身份验证信息。 我们将哈希密码并进行比较。 如果成功,则将用户的ID添加为cookie。 如果没有,我们将返回None

#[post("/users/login", format="json", data="<login_info>")]
fn login_post(
db: State<String>,
login_info: Json<LoginInfo>,
mut cookies: Cookies)
-> Json<Option<i32>> {
let maybe_user = fetch_user_by_email(&db, &login_info.username);
match maybe_user {
Some(user) => {
let maybe_auth = fetch_auth_info_by_user_id(&db, user.id);
match maybe_auth {
Some(auth_info) => {
let hash = hash_password(&login_info.password);
if hash == auth_info.password_hash {
cookies.add_private(Cookie::new(
"user_id", u ser.id.to_string()));
Json(Some(user.id))
} else {
Json(None)
}
}
None => Json(None)
}
}
None => Json(None)
}
}

A more robust solution of course would loop in some error behavior instead of returning None.

当然,更可靠的解决方案将循环出现某些错误行为,而不是返回None

使用Cookies (Using Cookies)

Using our cookie now is pretty easy. Let’s make a separate “fetch user” endpoint using our cookies. It will take the Cookies object and the user ID as inputs. The first order of business is to retrieve the user_id cookie and verify it exists.

现在使用我们的cookie非常简单。 让我们使用Cookie创建一个单独的“获取用户”端点。 它将Cookies对象和用户ID作为输入。 首先user_id是检索user_id cookie并确认它存在。

#[get("/users/cookies/<uid>")]
fn fetch_special(db: State<String>, uid: i32, mut cookies: Cookies)
-> Json<Option<UserEntity>> {
let logged_in_user = cookies.get_private("user_id");
match logged_in_user {
Some(c) => {
...
},
None => Json(None)
}}

Now we need to parse the string value as a user ID and compare it to the value from the endpoint. If they’re a match, we just fetch the user’s information from our database!

现在,我们需要将字符串值解析为用户ID,并将其与端点中的值进行比较。 如果他们是匹配的,我们只是从数据库中获取用户的信息!

#[get("/users/cookies/<uid>")]
fn fetch_special(db: State<String>, uid: i32, mut cookies: Cookies)
-> Json<Option<UserEntity>> {
let logged_in_user = cookies.get_private("user_id");
match logged_in_user {
Some(c) => {
let logged_in_uid = c.value().parse::<i32>().unwrap();
if logged_in_uid == uid {
Json(fetch_user_by_id(&db, uid))
} else {
Json(None)
}
},
None => Json(None)
}

And when we’re done, we can also post a “logout” request that will remove the cookie!

完成后,我们还可以发布“注销”请求,以删除Cookie!

#[post("/users/logout", format="json")]
fn logout(mut cookies: Cookies) -> () {
cookies.remove_private(Cookie::named("user_id"));
}

结论 (Conclusion)

We’ve got one more article on Rocket before checking out some different Rust concepts. So far, we’ve only dealt with the backend part of our API. Next week, we’ll investigate how we can use Rocket to send templated HTML files and other static web content!

在检查一些不同的Rust概念之前,我们还会在Rocket上发表另一篇文章。 到目前为止,我们只处理了API的后端部分。 下周,我们将研究如何使用Rocket发送模板HTML文件和其他静态Web内容!

Maybe you’re more experienced with Haskell but still need a bit of an introduction to Rust. We’ve got some other materials for you! Watch our Rust Video Tutorial for an in-depth look at the basics of the language!

也许您对Haskell更有经验,但是仍然需要对Rust进行一些介绍。 我们还有其他材料供您选择! 观看我们的Rust视频教程 ,深入了解该语言的基础知识!

附录:从请求实施 (Appendix: From Request Implementation)

impl<'a, 'r> FromRequest<'a, 'r> for AuthenticatedUser {
type Error = LoginError;
fn from_request(request: &'a Request<'r>) -> Outcome<AuthenticatedUser, LoginError> {
let username = request.headers().get_one("username");
let password = request.headers().get_one("password");
match (username, password) {
(Some(u), Some(p)) => {
let conn_str = local_conn_string();
let maybe_user = fetch_user_by_email(&conn_str, &String::from(u));
match maybe_user {
Some(user) => {
let maybe_auth_info = fetch_auth_info_by_user_id(&conn_str, user.id);
match maybe_auth_info {
Some(auth_info) => {
let hash = hash_password(&String::from(p));
if hash == auth_info.password_hash {
Outcome::Success(AuthenticatedUser{user_id: 1})
} else {
Outcome::Failure((Status::Forbidden, LoginError::WrongPassword))
}
}
None => {
Outcome::Failure((Status::MovedPermanently, LoginError::WrongPassword))
}
}
}
None => Outcome::Failure((Status::NotFound, LoginError::UsernameDoesNotExist))
}
},
_ => Outcome::Failure((Status::BadRequest, LoginError::InvalidData))
}
}
}

翻译自: https://medium.com/@james_32022/authentication-in-rocket-feb4f7223254

idea火箭

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值