Rust 使用 Actix-Web 验证 Auth Web 微服务 - 完整教程第 1 部分

2018-11-12 21:39:05 +08:00
 krircc

文章出自 Rust 中文社区

我们将创建一个 rust 仅处理用户注册和身份验证的 Web 服务器。我将在逐步解释每个文件中的步骤。完整的项目代码在这里

事件的流程如下所示:

我们打算使用的包

我从他们的官方说明中提供了有关正在使用的包的简要信息。如果您想了解更多有关这些板条箱的信息,请转到 crates.io

准备

我将在这里假设您对编程有一些了解,最好还有一些 Rust。需要进行工作设置 rust。查看https://rustup.rs用于 rust 设置。

我们将使用diesel来创建模型并处理数据库,查询和迁移。请前往http://diesel.rs/guides/getting-started/开始使用并进行设置diesel_cli。在本教程中我们将使用 postgresql,请按照说明设置 postgres。您需要有一个正在运行的 postgres 服务器,并且可以创建一个数据库来完成本教程。另一个很好的工具是Cargo Watch,它允许您在进行任何更改时观看文件系统并重新编译并重新运行应用程序。

如果您的系统上已经没有安装Curl,请在本地测试 api。

让我们开始

检查你的 rust 和 cargo 版本并创建一个新的项目

# at the time of writing this tutorial my setup is 
rustc --version && cargo --version
# rustc 1.29.1 (b801ae664 2018-09-20)
# cargo 1.29.0 (524a578d7 2018-08-05)

cargo new simple-auth-server
# Created binary (application) `simple-auth-server` project

cd simple-auth-server # and then 

# watch for changes re-compile and run
cargo watch -x run 

用以下内容填写 cargo 依赖关系,我将在项目中使用它们。我正在使用 crate 的显式版本,因为你知道包变旧了并且发生了变化。(如果你在很长一段时间之后阅读本教程)。在本教程的第 1 部分中,我们不会使用所有这些,但它们在最终的应用程序中都会变得很方便。

[dependencies]
actix = "0.7.4"
actix-web = "0.7.8"
bcrypt = "0.2.0"
chrono = { version = "0.4.6", features = ["serde"] }
diesel = { version = "1.3.3", features = ["postgres", "uuid", "r2d2", "chrono"] }
dotenv = "0.13.0"
env_logger = "0.5.13"
failure = "0.1.2"
frank_jwt = "3.0"
futures = "0.1"
r2d2 = "0.8.2"
serde_derive="1.0.79"
serde_json="1.0"
serde="1.0"
sparkpost = "0.4"
uuid = { version = "0.6.5", features = ["serde", "v4"] }

设置基本 APP

创建新文件 src/models.rs 与 src/app.rs

// models.rs
use actix::{Actor, SyncContext};
use diesel::pg::PgConnection;
use diesel::r2d2::{ConnectionManager, Pool};

/// This is db executor actor. can be run in parallel
pub struct DbExecutor(pub Pool<ConnectionManager<PgConnection>>);


// Actors communicate exclusively by exchanging messages. 
// The sending actor can optionally wait for the response. 
// Actors are not referenced directly, but by means of addresses.
// Any rust type can be an actor, it only needs to implement the Actor trait.
impl Actor for DbExecutor {
    type Context = SyncContext<Self>;
}

要使用此 Actor,我们需要设置 actix-web 服务器。我们有以下内容 src/app.rs 。我们暂时将资源构建者留空。这就是路由的核心所在。

// app.rs
use actix::prelude::*;
use actix_web::{http::Method, middleware, App};
use models::DbExecutor;

pub struct AppState {
    pub db: Addr<DbExecutor>,
}

// helper function to create and returns the app after mounting all routes/resources
pub fn create_app(db: Addr<DbExecutor>) -> App<AppState> {
    App::with_state(AppState { db })
        // setup builtin logger to get nice logging for each request
        .middleware(middleware::Logger::new("\"%r\" %s %b %Dms"))

         // routes for authentication
        .resource("/auth", |r| {
        })
        // routes to invitation
        .resource("/invitation/", |r| {
        })
        // routes to register as a user after the
        .resource("/register/", |r| {
        })
}

main.rs

// main.rs
// to avoid the warning from diesel macros
#![allow(proc_macro_derive_resolution_fallback)]

extern crate actix;
extern crate actix_web;
extern crate serde;
extern crate chrono;
extern crate dotenv;
extern crate futures;
extern crate r2d2;
extern crate uuid;
#[macro_use] extern crate diesel;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate failure;

mod app;
mod models;
mod schema;
// mod errors;
// mod invitation_handler;
// mod invitation_routes;

use models::DbExecutor;
use actix::prelude::*;
use actix_web::server;
use diesel::{r2d2::ConnectionManager, PgConnection};
use dotenv::dotenv;
use std::env;


fn main() {
    dotenv().ok();
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let sys = actix::System::new("Actix_Tutorial");

    // create db connection pool
    let manager = ConnectionManager::<PgConnection>::new(database_url);
    let pool = r2d2::Pool::builder()
        .build(manager)
        .expect("Failed to create pool.");

    let address :Addr<DbExecutor>  = SyncArbiter::start(4, move || DbExecutor(pool.clone()));

    server::new(move || app::create_app(address.clone()))
        .bind("127.0.0.1:3000")
        .expect("Can not bind to '127.0.0.1:3000'")
        .start();

    sys.run();
}

在此阶段,您的服务器应该编译并运行 127.0.0.1:3000。让我们创建一些模型。

设置 diesel 并创建我们的用户模型

我们首先为用户创建模型。假设您已经完成 postgres 并 diesel-cli 安装并正常工作。在您的终端中echo DATABASE_URL=postgres://username:password@localhost/database_name > .env,在设置时替换database_name,username 和 password。然后我们在终端跑diesel setup。这将创建我们的数据库并设置迁移目录等。

我们来写一些吧 SQL。通过diesel migration generate users和创建迁移diesel migration generate invitations。在migrations文件夹中打开up.sqldown.sql文件,并分别添加以下 sql。

--migrations/TIMESTAMP_users/up.sql
CREATE TABLE users (
  email VARCHAR(100) NOT NULL PRIMARY KEY,
  password VARCHAR(64) NOT NULL, --bcrypt hash
  created_at TIMESTAMP NOT NULL
);

--migrations/TIMESTAMP_users/down.sql
DROP TABLE users;

--migrations/TIMESTAMP_invitations/up.sql
CREATE TABLE invitations (
  id UUID NOT NULL PRIMARY KEY,
  email VARCHAR(100) NOT NULL,
  expires_at TIMESTAMP NOT NULL
);

--migrations/TIMESTAMP_invitations/down.sql
DROP TABLE invitations;

在您的终端中 diesel migration run将在 DB 和 src/schema.rs 文件中创建表。这将进行 diesel 和 migrations。请阅读他们的文档以了解更多信息。

在这个阶段,我们已经在 db 中创建了表,让我们编写一些代码来创建usersinvitations的表示。在 models.rs 我们添加以下内容。

// models.rs
...
// --- snip
use chrono::NaiveDateTime;
use uuid::Uuid;
use schema::{users,invitations};

#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
#[table_name = "users"]
pub struct User {
    pub email: String,
    pub password: String,
    pub created_at: NaiveDateTime, // only NaiveDateTime works here due to diesel limitations
}

impl User {
    // this is just a helper function to remove password from user just before we return the value out later
    pub fn remove_pwd(mut self) -> Self {
        self.password = "".to_string();
        self
    }
}

#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
#[table_name = "invitations"]
pub struct Invitation {
    pub id: Uuid,
    pub email: String,
    pub expires_at: NaiveDateTime,
}

检查您的实现是否没有错误 /警告,并密切关注终端中的cargo watch -x run命令。

我们自己的错误响应类型

在我们开始为应用程序的各种路由实现处理程序之前,我们首先设置一般错误响应。它不是强制性要求,但随着您的应用程序的增长,将来可能会有用。

Actix-web 提供与failure库的自动兼容性,以便错误导出失败将自动转换为 actix 错误。请记住,这些错误将使用默认的 500 状态代码呈现,除非您还为它们提供了自己的error_response ()实现。

这将允许我们使用自定义消息发送 http 错误响应。创建errors.rs包含以下内容的文件。

// errors.rs
use actix_web::{error::ResponseError, HttpResponse};


#[derive(Fail, Debug)]
pub enum ServiceError {
    #[fail(display = "Internal Server Error")]
    InternalServerError,

    #[fail(display = "BadRequest: {}", _0)]
    BadRequest(String),
}

// impl ResponseError trait allows to convert our errors into http responses with appropriate data
impl ResponseError for ServiceError {
    fn error_response(&self) -> HttpResponse {
        match *self {
            ServiceError::InternalServerError => {
                HttpResponse::InternalServerError().json("Internal Server Error")
            },
            ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message),
        }
    }
}

不要忘记添加mod errors;到您的main.rs文件中以便能够使用自定义错误消息。

实现handler处理程序

我们希望我们的服务器从客户端收到一封电子邮件,并在数据库中的invitations表中创建。在此实现中,我们将向用户发送电子邮件。如果您没有设置电子邮件服务,则可以忽略电子邮件功能,只需使用服务器上的响应数据。

从 actix 文档:

Actor 通过发送消息与其他 actor 通信。在 actix 中,所有消息具有类型。消息可以是实现Message trait的任何 Rust 类型。

并且

请求处理程序可以是实现Handler trait的任何对象。请求处理分两个阶段进行。首先调用 handler 对象,返回实现Responder trait的任何对象。然后,在返回的对象上调用respond_to (),将自身转换为AsyncResultError

让我们实现 Handler 这样的请求。首先创建一个新文件src/invitation_handler.rs并在其中创建以下结构。

// invitation_handler.rs
use actix::{Handler, Message};
use chrono::{Duration, Local};
use diesel::result::{DatabaseErrorKind, Error::DatabaseError};
use diesel::{self, prelude::*};
use errors::ServiceError;
use models::{DbExecutor, Invitation};
use uuid::Uuid;

#[derive(Debug, Deserialize)]
pub struct CreateInvitation {
    pub email: String,
}

// impl Message trait allows us to make use if the Actix message system and
impl Message for CreateInvitation {
    type Result = Result<Invitation, ServiceError>;
}

impl Handler<CreateInvitation> for DbExecutor {
    type Result = Result<Invitation, ServiceError>;

    fn handle(&mut self, msg: CreateInvitation, _: &mut Self::Context) -> Self::Result {
        use schema::invitations::dsl::*;
        let conn: &PgConnection = &self.0.get().unwrap();

        // creating a new Invitation object with expired at time that is 24 hours from now
        // this could be any duration from current time we will use it later to see if the invitation is still valid
        let new_invitation = Invitation {
            id: Uuid::new_v4(),
            email: msg.email.clone(),
            expires_at: Local::now().naive_local() + Duration::hours(24),
        };

        diesel::insert_into(invitations)
            .values(&new_invitation)
            .execute(conn)
            .map_err(|error| {
                println!("{:#?}",error); // for debugging purposes
                ServiceError::InternalServerError
            })?;

        let mut items = invitations
            .filter(email.eq(&new_invitation.email))
            .load::<Invitation>(conn)
            .map_err(|_| ServiceError::InternalServerError)?;

        Ok(items.pop().unwrap())
    }
}

不要忘记在main.rs文件中添加mod invitation_handler

现在我们有一个处理程序来插入和返回 DB 的invitations。使用以下内容创建另一个文件。register_email()接收CreateInvitation结构和保存 DB 地址的状态。我们通过调用into_inner ()发送实际的signup_invitation结构。此函数以异步方式返回invitations或我们的Handler处理程序中定义的错误.

// invitation_routes.rs

use actix_web::{AsyncResponder, FutureResponse, HttpResponse, Json, ResponseError, State};
use futures::future::Future;

use app::AppState;
use invitation_handler::CreateInvitation;

pub fn register_email((signup_invitation, state): (Json<CreateInvitation>, State<AppState>))
    -> FutureResponse<HttpResponse> {
    state
        .db
        .send(signup_invitation.into_inner())
        .from_err()
        .and_then(|db_response| match db_response {
            Ok(invitation) => Ok(HttpResponse::Ok().json(invitation)),
            Err(err) => Ok(err.error_response()),
        }).responder()
}

测试你的服务器

你应该能够使用以下 curl 命令测试http://localhost:3000/invitation路由。

curl --request POST \
  --url http://localhost:3000/invitation \
  --header 'content-type: application/json' \
  --data '{"email":"test@test.com"}'
# result would look something like
{
    "id": "67a68837-a059-43e6-a0b8-6e57e6260f0d",
    "email": "test@test.com",
    "expires_at": "2018-10-23T09:49:12.167510"
}

结束第 1 部分

在下一部分中,我们将扩展我们的应用程序以生成电子邮件并将其发送给注册用户进行验证。我们还允许用户在验证后注册和验证。

英文原文

3910 次点击
所在节点    Rust
5 条回复
trait
2018-11-12 21:53:07 +08:00
rust 不是有个 rustcc 的中文社区么,还有一个 rust-china,为啥还要到处造“社区轮子”。。。
krircc
2018-11-12 21:56:56 +08:00
@trait 那些轮子现在好用不
trait
2018-11-12 22:04:06 +08:00
@krircc rustcc 还行
krircc
2018-11-12 22:09:56 +08:00
@trait 要翻墙
ltoddy
2018-11-12 22:42:32 +08:00
我看了一下,为啥建表要自己写 sql 建表,建表,数据迁移啥的不应该上 orm 自动做的嘛。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/507161

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX