سیستم احراز هویت با استفاده از Rust (actix-web) و sveltekit – Backend Intro

معرفی
سلام بچه ها! اینجا مدتی است. من در مدتی که نبودم کمی زنگ زدگی را یاد گرفتم و برخی از چیزهایی را که یاد گرفتم به اشتراک خواهم گذاشت.
سیستم احراز هویت بخشی جدایی ناپذیر از برنامه های کاربردی مدرن است. این بسیار مهم است که تقریباً همه برنامه های مدرن نوعی از آن را دارند. به دلیل ماهیت حیاتی آنها، چنین سیستم هایی باید ایمن باشند و باید از توصیه های OWAP® در مورد امنیت وب و هش رمز عبور و همچنین ذخیره سازی برای جلوگیری از حملاتی مانند حملات Preimage و Dictionary (که در الگوریتم های SHA رایج است) پیروی کنند. برای نشان دادن برخی از توصیهها، ما یک سیستم احراز هویت مبتنی بر جلسه قوی در Rust و یک برنامه فرانتاند تکمیلی خواهیم ساخت. برای این سری مقالات، ما از Actix-Web Rust و چند جعبه عالی برای سرویس Backend استفاده خواهیم کرد. SvelteKit برای قسمت جلویی استفاده خواهد شد. با این حال، باید توجه داشت که آنچه که ما خواهیم ساخت تا حد زیادی چارچوبی است. در نتیجه، میتوانید تصمیم بگیرید که axum، rocket، warp یا هر چارچوب وب rust دیگری را برای backend انتخاب کنید و react، vue یا هر چارچوب جاوا اسکریپت دیگری را برای frontend انتخاب کنید. حتی می توانید از سرخدار، دانه یا برخی موتورهای قالب مانند MiniJinja یا tera در قسمت جلویی استفاده کنید. این کاملا به شما بستگی دارد. تمرکز ما بیشتر بر روی مفاهیم خواهد بود.
توجه: ما از کتاب صفر تا تولید در زنگ، به شدت با برخی ویژگیها و تغییرات اضافی استفاده خواهیم کرد.
اگرچه ما در حال ساخت یک سیستم احراز هویت مبتنی بر جلسه هستیم، قابل توجه است که با معرفی برخی از مفاهیم که در زمان مناسب مورد بحث قرار خواهند گرفت، میتوانید آن را به سیستم احراز هویت مبتنی بر JWT یا ایمنتر و مناسبتر تبدیل کنید.
توجه: این آموزش به چند مقاله کوتاه تقسیم می شود. حداقل یک (1) مقاله هر هفته بارگذاری می شود تا کل مجموعه کامل شود.
مشخصات مورد نیاز سیستم
در طول این مجموعه آموزشی، ما در جهت اجرای این الزامات کار خواهیم کرد:
یک سیستم احراز هویت کاربر بسازید که در آن کاربر با ترکیب ایمیل/رمز عبور احراز هویت می کند. آدرس های ایمیل باید باشد منحصر بفرد و تایید شده است پس از ثبت نام و تأیید، با ارسال ایمیل های تأیید با زمان محدود ایمیل ها باید از HTML پشتیبانی کنند. تا زمان تایید، هیچ کاربری اجازه ورود به سیستم را ندارد. حملات زمانی باید توسط ارسال نامه ها به صورت ناهمزمان. هش رمز عبور باید قوی باشد و فقط رمزهای عبور هش شده باید در پایگاه داده ذخیره شوند. قابلیت بازنشانی گذرواژه باید با استفاده از تأیید آدرس پست الکترونیکی گنجانده شده و پذیرفته شود. یک ویژگی به روز رسانی نمایه کاربر محافظت شده باید اضافه شود تا فقط کاربران تأیید شده و مجاز بتوانند به آن دسترسی داشته باشند. نمایه کاربر باید شامل یک تصویر کوچک باشد که باید در AWS S3 ذخیره شود.
😲 این خیلی بود، نه؟! از این انتها هم هست 😫😩 . از مشخصات، ما ملزم به داشتن مقداری سرگرمی هستیم. ما به سمت بالا حرکت خواهیم کرد و قلمرو بارگذاری تصویر در AWS S3، تأیید ایمیل، تولید و تخریب توکن، برخی از الگوها و تعداد زیادی از موارد دیگر را ترسیم خواهیم کرد.
پشته فناوری
برای تأکید، پشته فناوری ما شامل موارد زیر است:
-
Backend – برخی از جعبه هایی که مورد استفاده قرار خواهند گرفت عبارتند از:
-
Frontend – برخی از ابزارهایی که مورد استفاده قرار خواهند گرفت عبارتند از:
فرض
یک پیش نیاز ساده برای دنبال کردن، آشنایی با زبان برنامه نویسی Rust است – مانند درک ساختارها، مدل مالکیت، بررسی کننده قرض، سیستم ماژول و سایر موارد. – جاوا اسکریپت (Typescript) و CSS. شما نیازی به متخصص بودن ندارید – من در هیچ یک از فناوری ها نیستم.
کد منبع
کد منبع این سری در github میزبانی می شود یا بیشتر از طریق:
یک سیستم احراز هویت تمام پشته با استفاده از rust، sveltekit و Typescript
ساختار اولیه پروژه
شما می توانید این قالب کامل استارت را از github دریافت کنید.
من ساختار خدمات وب Rust را از صفر تا تولید در Rust به ارث بردم. من عاشق این ساختار شده ام و به احتمال زیاد از آن برای اکثر پروژه های وب Rust خود صرف نظر از چارچوب انتخابی خود استفاده خواهم کرد. این الگوی شروع در اینجا موجود است و نحوه ساخت آن به طور خلاصه مورد بحث قرار خواهد گرفت. من شما را تشویق می کنم که کتاب صفر تا تولید در زنگ را بردارید. فوق العاده است!!! در حال حاضر، backend
ساختار به شکل زیر است:
├── Cargo.lock
├── Cargo.toml
├── settings
│ ├── base.yaml
│ ├── development.yaml
│ └── production.yaml
├── src
│ ├── lib.rs
│ ├── main.rs
│ ├── routes
│ │ ├── health.rs
│ │ └── mod.rs
│ ├── settings.rs
│ ├── startup.rs
│ └── telemetry.rs
└── tests
مرحله 1: یک پروژه جدید ایجاد کنید و برخی از وابستگی ها را نصب کنید
دایرکتوری ایجاد کنید که کل برنامه (هم جلو و هم باطن) را در خود جای دهد. به مال خودم زنگ زدم rust-auth
. دایرکتوری را به پوشه تازه ایجاد شده تغییر دهید و دستور زیر را در ترمینال خود صادر کنید:
~/rust-auth$ cargo new backend
این یک پروژه جدید به نام ایجاد می کند backend
با Cargo.toml
، Cargo.lock
، و src/main.rs
فایل های ایجاد شده آن را در ویرایشگر انتخابی خود باز کنید. خود را بسازید Cargo.toml
فایل شبیه به این است:
# Cargo.toml
[package]
name = "backend"
version = "0.1.0"
authors = ["Your name <your email>"]
edition = "2021"
[lib]
path = "src/lib.rs"
[[bin]]
path = "src/main.rs"
name = "backend"
[dependencies]
actix-web = "4"
config = { version = "0.13.3", features = ["yaml"] }
dotenv = "0.15.0"
serde = "1.0.160"
tokio = { version = "1.27.0", features = ["macros", "rt-multi-thread"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.17", features = [
"fmt",
"std",
"env-filter",
"registry",
'json',
'tracing-log',
] }
اضافه کردیم authors
به [package]
بخش. سپس یک بخش جدید ایجاد کردیم، [lib]
، که به مسیر پروژه اشاره می کند lib.rs
فایل. یک پروژه می تواند تنها یک مورد داشته باشد lib.rs
فایل. بعد بخش باینری است، [[bin]]
. دو براکت مربع داخل .toml
فایل ها به معنای آرایه هستند. به این دلیل استفاده شد که میتوانیم بیش از یک بسته باینری در پروژه Rust داشته باشیم. این دو بخش جدید نوشتن تست یکپارچه یکپارچه را برای ما آسانتر میکنند که در آن تستها “مستقل” از چارچوب وب مورد استفاده هستند. سپس [dependencies]
بخش. جعبه های اولیه ای را که استفاده خواهیم کرد ثبت کردیم. config
به تغییر آسان کمک می کند .yaml
یا .json
فایلهایی که حاوی برخی تنظیمات در سطح برنامه هستند، مانند متغیرهای موجود در جنگو settings.py
فایل، به ساختارهای زنگ. dotenv
بارگذاری متغیرهای محیطی از a .env
فایل. serde
چارچوب سریالسازی/آسیالزدایی عمومی Rust است. tokio
یک زمان اجرا استاندارد صنعتی برای نوشتن برنامه های کاربردی قابل اعتماد، ناهمزمان و باریک با زنگ زدگی است. در زمان اجرا یا زمانی که برنامه ما در حال تولید است، در نهایت باید درخواست ها و پاسخ ها را ثبت کنیم. گاهی اوقات، کاربران ما شکایت می کنند یا برنامه ما خراب می شود. ما نمی توانیم فقط بفهمیم که چرا وضعیت خاصی از هوا رخ می دهد. ما به یک نقطه مرجع برای رفع اشکال برنامه خود نیاز داریم. در اکوسیستم زنگ، tracing
و گسترش آن tracing-subscriber
به طور گسترده برای این مورد استفاده می شود. Telemetry
چیزی است که به آن می گویند.
مرحله 2: اسکلت پروژه را بسازید
درون src
پوشه، دستورات زیر را برای ایجاد چند فایل و پوشه صادر کنید:
~/rust-auth/backend$ touch src/lib.rs src/startup.rs src/settings.rs src/telemetry.rs
~/rust-auth/backend$ mkdir src/routes && touch src/routes/mod.rs src/routes/health.rs
برای اینکه فایلها و پوشههای ایجاد شده شناسایی شوند، باید آنها را به ماژول تبدیل کنیم lib.rs
:
// src/lib.rs
pub mod routes;
pub mod settings;
pub mod startup;
pub mod telemetry;
بیا شروع کنیم با telemetry.rs
. آن را به شکل زیر در بیاورید:
// src/telemetry.rs
use tracing_subscriber::layer::SubscriberExt;
pub fn get_subscriber(debug: bool) -> impl tracing::Subscriber + Send + Sync {
let env_filter = if debug {
"trace".to_string()
} else {
"info".to_string()
};
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(env_filter));
let stdout_log = tracing_subscriber::fmt::layer().pretty();
let subscriber = tracing_subscriber::Registry::default()
.with(env_filter)
.with(stdout_log);
let json_log = if !debug {
let json_log = tracing_subscriber::fmt::layer().json();
Some(json_log)
} else {
None
};
let subscriber = subscriber.with(json_log);
subscriber
}
pub fn init_subscriber(subscriber: impl tracing::Subscriber + Send + Sync) {
tracing::subscriber::set_global_default(subscriber).expect("Failed to set subscriber");
}
ما در حال پیکربندی هستیم tracing_subscriber
سطح و قالب بسته به اینکه آیا برنامه ما در حال تولید است یا خیر. اگر debug
است true
، سپس در حالت توسعه هستیم. در غیر این صورت، در تولید. ما میخواهیم JSON
خروجی در تولید، زیرا تجزیه آن آسان تر است. سپس، ردیابی را در تابع دیگری بر اساس مشترک داده شده مقداردهی اولیه کردیم.
بعد، settings.rs
:
// src/settings.rs
/// Global settings for exposing all preconfigured variables
#[derive(serde::Deserialize, Clone)]
pub struct Settings {
pub application: ApplicationSettings,
pub debug: bool,
}
/// Application's specific settings to expose `port`,
/// `host`, `protocol`, and possible URL of the application
/// during and after development
#[derive(serde::Deserialize, Clone)]
pub struct ApplicationSettings {
pub port: u16,
pub host: String,
pub base_url: String,
pub protocol: String,
}
/// The possible runtime environment for our application.
pub enum Environment {
Development,
Production,
}
impl Environment {
pub fn as_str(&self) -> &'static str {
match self {
Environment::Development => "development",
Environment::Production => "production",
}
}
}
impl TryFrom<String> for Environment {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
match s.to_lowercase().as_str() {
"development" => Ok(Self::Development),
"production" => Ok(Self::Production),
other => Err(format!(
"{} is not a supported environment. Use either `development` or `production`.",
other
)),
}
}
}
/// Multipurpose function that helps detect the current environment the application
/// is running using the `APP_ENVIRONMENT` environment variable.
///
/// \`\`\`
/// APP_ENVIRONMENT = development | production.
/// \`\`\`
///
/// After detection, it loads the appropriate .yaml file
/// then it loads the environment variable that overrides whatever is set in the .yaml file.
/// For this to work, you the environment variable MUST be in uppercase and starts with `APP`,
/// a `_` separator then the category of settings,
/// followed by `__` separator, and then the variable, e.g.
/// `APP__APPLICATION_PORT=5001` for `port` to be set as `5001`
pub fn get_settings() -> Result<Settings, config::ConfigError> {
let base_path = std::env::current_dir().expect("Failed to determine the current directory");
let settings_directory = base_path.join("settings");
// Detect the running environment.
// Default to `development` if unspecified.
let environment: Environment = std::env::var("APP_ENVIRONMENT")
.unwrap_or_else(|_| "development".into())
.try_into()
.expect("Failed to parse APP_ENVIRONMENT.");
let environment_filename = format!("{}.yaml", environment.as_str());
let settings = config::Config::builder()
.add_source(config::File::from(settings_directory.join("base.yaml")))
.add_source(config::File::from(
settings_directory.join(environment_filename),
))
// Add in settings from environment variables (with a prefix of APP and '__' as separator)
// E.g. `APP_APPLICATION__PORT=5001 would set `Settings.application.port`
.add_source(
config::Environment::with_prefix("APP")
.prefix_separator("_")
.separator("__"),
)
.build()?;
settings.try_deserialize::<Settings>()
}
ما چند ساختار و یک enum داریم. این ساختارها مستقیماً به .yaml
فایل هایی که به زودی ایجاد خواهیم کرد. بخش عمده ای از کار در این است settings.rs
فایل در get_settings
تابع. هر زمان که به برخی از متغیرهای تنظیمات نیاز داشته باشیم، با فراخوانی این تابع آنها را دریافت خواهیم کرد. با نگاهی به داخل، ابتدا سعی می کنیم مسیر دایرکتوری را که در آن قرار داریم به دست آوریم .yaml
فایل ها قرار دارند. سپس تشخیص می دهیم که آیا در حال توسعه هستیم یا خیر. به طور پیش فرض، ما فرض می کنیم که برنامه در حال توسعه است. برای تغییر آن به تولید، باید تنظیم کنید APP_ENVIRONMENT=production
در شما .env
فایل یا هر روش دیگری که متغیرهای محیط خود را تنظیم می کنید. از آنجایی که محیط های توسعه و تولید برخی از متغیرها را به اشتراک می گذارند – ما آنها را در آنها ذخیره می کنیم base.yaml
– ما از خود استفاده می کنیم config
جعبه تا ابتدا آن پیکربندیهای رایج را قبل از بارگیری تنظیمات خاص محیط بارگیری کنید. این به این دلیل است که ما احتمالاً آن پیکربندیهای رایج را بر اساس هر محیطی نادیده میگیریم. برخی از تنظیمات در وجود دارد .yaml
فایل هایی که ممکن است بخواهیم مقادیر آنها را با استفاده از متغیرهای محیطی تغییر دهیم. توکن ها، رمزهای عبور و secret_key چند نمونه هستند. ما می خواهیم آنهایی که از طریق متغیرهای محیطی تنظیم شده اند اولویت داشته باشند. مثلاً اگر تنظیم کنیم debug: true
که در base.yaml
اما در تولید، ما می خواهیم debug: false
. ما فقط می توانیم انجام دهیم APP_DEBUG=true
در ما .env
را فایل کنید و این یکی را لغو می کند base.yaml
. به پیشوند توجه کنید، APP_
. لازم است که چنین چیزی به عنوان متغیر تنظیم شناخته شود. شما می توانید پیشوند را نیز تغییر دهید. با پیشرفت بیشتر در مورد این تفاوت های ظریف بیشتر می آموزیم. حال، بیایید ایجاد کنیم settings/
دایرکتوری در ریشه پروژه ما نیز ایجاد خواهیم کرد base.yaml
، development.yaml
و production.yaml
در آن:
~/rust-auth/backend$ mkdir settings && touch settings/base.yaml settings/development.yaml settings/production.yaml
فعلا بساز settings/base.yaml
به نظر می رسد این است:
# settings/base.yaml
application:
port: 5000
settings/development.yaml
:
# settings/development.yaml
application:
protocol: http
host: 127.0.0.1
base_url: "http://127.0.0.1"
debug: true
و، settings/production.yaml
:
# settings/production.yaml
application:
protocol: https
host: 0.0.0.0
base_url: ""
debug: false
بعدی است src/startup.rs
:
// src/startup.rs
pub struct Application {
port: u16,
server: actix_web::dev::Server,
}
impl Application {
pub async fn build(settings: crate::settings::Settings) -> Result<Self, std::io::Error> {
let address = format!(
"{}:{}",
settings.application.host, settings.application.port
);
let listener = std::net::TcpListener::bind(&address)?;
let port = listener.local_addr().unwrap().port();
let server = run(listener).await?;
Ok(Self { port, server })
}
pub fn port(&self) -> u16 {
self.port
}
pub async fn run_until_stopped(self) -> Result<(), std::io::Error> {
self.server.await
}
}
async fn run(listener: std::net::TcpListener) -> Result<actix_web::dev::Server, std::io::Error> {
let server = actix_web::HttpServer::new(move || {
actix_web::App::new().service(crate::routes::health_check)
})
.listen(listener)?
.run();
Ok(server)
}
کل برنامه ما را راه اندازی می کند و در آن انجام می شود run_until_stopped
روش از Application
ساخت. انگیزه نوشتن به این صورت برای تست آسان است. این تنها راه برای شروع نیست actix-web
سرور تمرین معمولی حداقل برای شروع بسیار کوتاهتر است. اما این کاملاً یک تصمیم طراحی است که اختیاری است.
وارد شویم src/main.rs
:
// src/main.rs
#[tokio::main]
async fn main() -> std::io::Result<()> {
dotenv::dotenv().ok();
let settings = backend::settings::get_settings().expect("Failed to read settings.");
let subscriber = backend::telemetry::get_subscriber(settings.clone().debug);
backend::telemetry::init_subscriber(subscriber);
let application = backend::startup::Application::build(settings).await?;
tracing::event!(target: "backend", tracing::Level::INFO, "Listening on http://127.0.0.1:{}/", application.port());
application.run_until_stopped().await?;
Ok(())
}
src/main.rs
نقطه ورود برنامه های Rust است. ما انتخاب می کنیم #[tokio::main]
زمان اجرا شما می توانید استفاده کنید #[actix_web::main]
بجای. بعد، آوردیم dotenv
به بازی برای کمک به بارگیری همه متغیرهای محیطی در ما .env
فایل. سپس تنظیمات خود را همانطور که در آن نوشته شده است دریافت می کنیم src/settings.rs
. سپس تله متری مقداردهی اولیه می شود. سپس کل برنامه ما ساخته شد و متعاقباً اجرا شد، اما قبل از اجرا، به توسعهدهنده درگاهی را که برنامه ما با استفاده از آن اجرا میشود، اطلاع دادیم. tracing::event
کلان. ما میتوانیم پورت را در اینجا دریافت کنیم زیرا آن را در دسترس خود قرار دادهایم src/startup.rs
. با این کار، ممکن است این فایل را لمس نکنیم، src/main.rs
، دوباره در طول این مجموعه. نقطه تماس ما خواهد بود src/startup.rs
.
اگر در این مرحله سعی کنید برنامه اسکلتی ما را اجرا کنید، هنوز کامپایل نمی شود. بیایید آن را درست کنیم.
هدایت به src/routes/health.rs
و آن را به شکل زیر در آورید:
// src/routes/health.rs
#[tracing::instrument]
#[actix_web::get("/health-check/")]
pub async fn health_check() -> actix_web::HttpResponse {
tracing::event!(target: "backend", tracing::Level::DEBUG, "Accessing health-check endpoint.");
actix_web::HttpResponse::Ok().json("Application is safe and healthy.")
}
ما یک نقطه پایانی ساده برای بررسی آنلاین بودن یا نبودن داریم. می توانید ببینید که نوشتن یک نقطه پایانی API در actix-web چقدر آسان است. جدا از ابزار دقیق، می توانید یک نقطه پایانی درخواست GET “کاملا کاربردی” را تنها با 3 خط کد سیم کشی کنید!!!
با نگاهی به نقطه پایانی، استفاده کردیم #[tracing::instrument]
برای کمک به حفظ گزارش تمام درخواست ها در این تابع. این ابزار دقیق است. سپس استفاده می کنیم #[actix_web::get("/health-check/")]
فقط به آن علامت دهد GET
درخواست ها مجاز است /health-check/
. هر روش دیگری مردود خواهد بود. یکی از دلایل استفاده از actix-web پشتیبانی بومی آن از توابع ناهمزمان همراه با این واقعیت است که بسیار سریع است. ما تابع خود را ناهمگام کردیم و انتظار داریم که تابع یک پاسخ HTTP برگرداند، actix_web::HttpResponse
. راه های دیگری نیز برای رسیدن به این هدف وجود دارد، اما من به دلیل کوتاه بودن این روش را ترجیح می دهم. سپس پیامی را برمی گردانیم، Application is safe and healthy.
، در قالب JSON به کاربر با استفاده از وضعیت HTTP Ok، 200. روش های دیگری برای پاسخ HTTP در actix-web موجود است و ما با برخی از آنها مواجه خواهیم شد.
در مرحله بعد، باید این روش را در دسترس قرار دهیم. باز کن src/routes/mod.rs
:
// src/routes/mod.rs
mod health;
pub use health::health_check;
این باعث می شود آن را برای عموم در دسترس قرار دهد. سپس آن را به عنوان یک سرویس در ثبت نام کردیم src/startup.rs
استفاده كردن crate::routes::health_check
:
// src/startup.rs
...
async fn run(listener: std::net::TcpListener) -> Result<actix_web::dev::Server, std::io::Error> {
let server = actix_web::HttpServer::new(move || {
actix_web::App::new().service(crate::routes::health_check)
})
.listen(listener)?
.run();
Ok(server)
}
همین برای اولین مقاله از سری!! همه شما را در بعدی می بینم.
دیگر
از این مقاله لذت بردید؟ در نظر بگیرید که برای یک کار، چیزی ارزشمند یا خرید یک قهوه با من تماس بگیرید. همچنین می توانید با من در لینکدین و دنبال کنید توییتر. بد نیست اگر به اشتراک گذاری این مقاله برای پوشش گسترده تر کمک کنید. قدردانش خواهم بود…