V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
krircc
V2EX  ›  Rust

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

  •  1
     
  •   krircc · 2018-11-12 21:39:05 +08:00 · 4107 次点击
    这是一个创建于 2207 天前的主题,其中的信息可能已经有所发展或是发生改变。

    文章出自 Rust 中文社区

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

    事件的流程如下所示:

    • 使用电子邮件地址注册➡接收带有链接的📨进行验证
    • 点击链接➡使相同的电子邮件和密码注册
    • 使用电子邮件和密码登录➡获取验证并接收 jwt 令牌

    我们打算使用的包

    • actix     // Actix 是一个 Rust actor 框架。
    • actix-web   // Actix web 是 Rust 的一个简单,实用且极其快速的 Web 框架。
    • brcypt     //使用 bcrypt 轻松散列和验证密码。
    • chrono     // Rust 的日期和时间库。
    • diesel     //用于 PostgreSQL,SQLite 和 MySQL 的安全,可扩展的 ORM 和查询生成器。
    • dotenv     // Rust 的 dotenv 实现。
    • env_logger   //通过环境变量配置的日志记录实现。
    • failure     //实验性错误处理抽象。
    • jsonwebtoken   //以强类型方式创建和解析 JWT。
    • futures     //future 和 Stream 的实现,具有零分配,可组合性和类似迭代器的接口。
    • r2d2      //通用连接池。
    • serde     //通用序列化 /反序列化框架。
    • serde_json   // JSON 序列化文件格式。
    • serde_derive   //#[derive ( Serialize,Deserialize )]的宏 1.1 实现。
    • sparkpost   //用于 sparkpost 电子邮件 api v1 的 Rust 绑定。
    • uuid     //用于生成和解析 UUID 的库。

    我从他们的官方说明中提供了有关正在使用的包的简要信息。如果您想了解更多有关这些板条箱的信息,请转到 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":"[email protected]"}'
    # result would look something like
    {
        "id": "67a68837-a059-43e6-a0b8-6e57e6260f0d",
        "email": "[email protected]",
        "expires_at": "2018-10-23T09:49:12.167510"
    }
    

    结束第 1 部分

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

    英文原文

    5 条回复    2018-11-12 22:42:32 +08:00
    trait
        1
    trait  
       2018-11-12 21:53:07 +08:00
    rust 不是有个 rustcc 的中文社区么,还有一个 rust-china,为啥还要到处造“社区轮子”。。。
    krircc
        2
    krircc  
    OP
       2018-11-12 21:56:56 +08:00
    @trait 那些轮子现在好用不
    trait
        3
    trait  
       2018-11-12 22:04:06 +08:00
    @krircc rustcc 还行
    krircc
        4
    krircc  
    OP
       2018-11-12 22:09:56 +08:00
    @trait 要翻墙
    ltoddy
        5
    ltoddy  
       2018-11-12 22:42:32 +08:00 via Android
    我看了一下,为啥建表要自己写 sql 建表,建表,数据迁移啥的不应该上 orm 自动做的嘛。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3452 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 10:58 · PVG 18:58 · LAX 02:58 · JFK 05:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.