原 薦 Rust使用Actix-Web驗證Auth Web微服務 - 完整教程第1部分
Rust使用Actix-Web驗證Auth Web微服務 - 完整教程第1部分
ofollow,noindex" target="_blank">文章出自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.sql
和 down.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中建立了表,讓我們編寫一些程式碼來建立 users
和 invitations
的表示。在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()
,將自身轉換為 AsyncResult
或 Error
。
讓我們實現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部分
在下一部分中,我們將擴充套件我們的應用程式以生成電子郵件並將其傳送給註冊使用者進行驗證。我們還允許使用者在驗證後註冊和驗證。
