پس از مدت طولانی، با یک پست دیگر برگشتم و این بار قراره با هم احراز هویت JWT رو در ریلز بررسی کنیم. اول از همه، لازمه بدونیم JWT چیه؟ چرا نیازش داریم؟ اصلا چرا احراز هویت و هزاران چرای دیگر که احتمالا الان در سر شما هستند. بعدش یک پروژه خیلی کوچولو ایجاد میکنیم و با هم براش احراز هویت رو پیادهسازی میکنیم 🙂
احراز هویت JWT چیه و چرا بهش نیاز داریم؟
اول از همه این سوال بنیادیتر رو پاسخ بدیم که «چرا احراز هویت نیاز داریم؟» و بعد بریم سراغ احراز هویت JWT که قراره در این مطلب در موردش مفصل حرف بزنیم.
ما به احراز هویت نیاز داریم چون همیشه بخشی از دادههای ما، خصوصی هستند. گذشته از اون، احراز هویت میتونه اجازه CRUD رو به شما بده، نه؟ فکر کنید اپی دارید که هرکسی میتونه روش بخونه و بنویسه. ممکنه خوندن چیزی باشه که برای «همه» مناسب باشه اما «نوشتن» اینطور نیست. بخصوص که نوشتن خودش به قسمتهایی مثل حذف و بروزرسانی هم شکسته شده.
پس ما به احراز هویت نیاز داریم که هر ننهقمری (😂) نتونه از API ما استفاده کنه. بلکه کاربرانی که ثبتنام کردند و دسترسی درستی به سرویس دارند، بتونن استفاده کنن. این قضیه در API های تجاری (یا Business facing) خودشون رو بیشتر و بهتر نشون میدن.
حالا سوال مهمتر …
احراز هویت JWT چیه؟
در اپهای قدیمی (مثل همین وردپرس)، احراز هویت توسط cookie ها انجام میشه. به چه صورتی؟ به این صورت که وقتی نام کاربری و گذرواژه وارد میکنیم، نرمافزار فضایی رو در مرورگر به خودش اختصاص میده و یک سری اطلاعات رو با خودش جابجا میکنه. اما در ReST API ها این قضیه رو نداریم. در واقع نمیتونیم به کوکیها اعتماد کنیم. پس چه میکنیم؟ اینجا لازمه جز یوزرنیم(که معمولا میتونه عمومی باشه) و پسورد (که میتونه راحت لو بره) میاییم و یک «توکن» هم تعریف میکنیم. این توکن، میتونه ثابت یا متغیر باشه. یعنی چی؟ یعنی میتونه به ازای هربار ورود تغییر کنه، میتونه سر زمان مشخصی هم منقضی بشه.
حالا توکن چیه؟ توکن به صورت کلی، در کازینوها معادل پولیه که شما در بازیها قرار میدید، در واقع مجوز حضور شما در اون کازینو، کلاب و … است. حتی به رمزارزهایی که بر مبنای دیگر رمزارزها ساخته میشن هم سکه نمیگن بلکه میگن توکن. شما فرض کنید که مثلا ۱۰۰ دلار میدید و پنج تا سکه با آرم اون کازینوی خاص دریافت میکنید. اگر در بازی برنده بشید یا ببازید، باید توکنهاتون رو تحویل بدید یا بگیرید.
حالا در احراز هویت JWT هم، ما به ازای کاربرمون یک توکن در نظر میگیریم. این توکن، معمولا یک رشته طولانیه که انسان نمیتونه بخوندش. نتیجتا خیلی از اطلاعات ما به صورت ایمنتر میتونن رد و بدل بشن (طبیعیه که مواردی مثل SSL داشتن و الگوریتمهایی که در ساخت توکن داشتیم هم مهمن). ضمن این که نامکاربری، ایمیل، رمزعبور و .. هم به همین سادگیا نمیتونن خونده بشن.
پس ما میآییم و یک دیتابیسی از توکنها در کنار دیتابیسی از یوزرها میسازیم (البته درستترش، جدوله!) و به ازای هر یوزر معمولا دوتا توکن در اون دیتابیس قرار میدیم. یکیش رو بهش میگیم «توکن دسترسی» یا Access Token و یکی رو میگیم «توکن بازنشانی» یا Refresh Token. توکن دسترسی، معمولا یک تاریخ انقضایی داره و بعد از اون با استفاده از توکن بازنشانی، میتونیم یکی جدید بگیریم. اما در آموزش امروز، صرفا میخوایم توکن دسترسی رو به دست بیاریم.
![شمایی از کار JWT](http://haghiri75.com/wp-content/uploads/jwt-1024x559.png)
خب، الان که تقریبا همهچی رو میدونیم، بریم برای پیادهسازی.
پیادهسازی یک اپلیکیشن ریلز با JWT
خب در قدم اول، باید یک اپ ایجاد کنیم. این اپ رو به این شکل ایجاد میکنیم:
rails new devise-jwt --api
خب توضیح واضحات:
- قسمت rails که واضحا فراخوانی نرمافزار rails در ترمینال ماست.
- قسمت new در خواست برای ایجاد یک اپ جدیده.
- قسمت devise-jwt اسم پروژه ماست. حالا چرا؟ چون قراره از یک لایبرری با همین اسم استفاده کنیم. بنابراین، پروژه رو اینطوری اسم گذاشتیم.
- در قسمت آخر هم، به ریلز گفتیم که ما تو رو بخاطر API هات دوست داریم. ویو نیاز نیست.
بعد از چند ثانیه (و بسته به سرعت اینترنت دقیقه) اپ ما ساخته میشه. بعد لازمه که مرحله مرحله کارهایی رو انجام بدیم.
نصب لایبرریهای مورد نیاز
gem 'devise' gem 'devise-jwt' gem 'rack-cors'
بعد از این که این خطوط رو اضافه کردیم، دستور زیر رو اجرا میکنیم:
bundle
این دستور چه کار میکنه؟ میاد و تمام لایبرریهای مورد نظر شما رو به صورت ایزوله در یک دایرکتوری، نصب میکنه. به این شکل شما میتونید به سادگی بدون رسیدن آسیب به باقی لایبرریهای روبی نصب شده روی سرور یا حتی کامپیوتر خودتون، ایدههاتون رو تست کنید.
لازم به ذکره که بعد از اجرای این دستور فایل Gemfile.lock بهروز میشه، این فایل حالا چه کار میکنه؟ این فایل حواسش به همهچی هست. در واقع، ورژن روبی، ورژن ریلز، لایبرریهای مورد نیاز و ورژنینگشون و … رو همه رو این فایل داره کنترل میکنه.
بعد از انجام مراحل فوق، کافیه این دستور هم اجرا کنیم:
rails g devise:install
این دستور چه میکنه؟ این دستور هم برای ما فایلهای devise رو در جای درستش قرار میده.
آشنایی با devise
برای احراز هویت در هر سیستمی ما دو راه داریم:
- نوشتن سیستم احراز هویت از بیخ
- استفاده از کتابخانههای موجود
در مورد روش «از بیخ»، ما معمولا این کار رو انجام نمیدیم. چرا؟ چون معمولا اونقدر خوب نیستیم که بتونیم امنیت سیستم رو به خوبی تامین کنیم. در مورد دوم، در هر فرمورک و زبانی، کتابخانههایی ساخته شدند که کمک میکنن ما بتونیم با اضافه کردنشون به پروژه خودمون، بخش احراز هویت رو هندل کنیم. برای ریلز devise ساخته شده. این لایبرری، یک لایبرری مبتنی بر cookie برای احراز هویت وباپهاست.
بعد از همهگیر شدن ReST API ها، لایبرری devise-jwt هم نوشته شد. این لایبرری، ابزاریه که به من و شما کمک میکنه بتونیم احراز هویت JWT رو به پروژهمون اضافه کنیم. در واقع هر سه لایبرری که به پروژه اضافه کردیم، کارشون همینه که JWT رو برای ما راحت کنند.
هندل کردن CORS
در این مطلب قصد ندارم در مورد CORS حرف بزنم، چون قبلتر ازش حرف زدم (و میتونید اینجا بخونید). اینجا ما صرفا قصدمون اینه که بیاییم و این مشکل رو حل کنیم. چطوری؟ خب این فایل:
config/initializers/cors.rb
رو با ویرایشگر متنی مورد علاقهمون باز میکنیم، و محتواش رو به این شکل تغییر میدیم:
Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins '*' resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end
توجه کنید که این قسمت معمولا به صورت کامنتشده در کد هست، فقط کافیه آنکامنتش کنید و نیاز نیست که کلا این مورد رو کپی پیست کنید.
ساخت مدل و کنترلرهای مورد نیاز برای کاربر
یکی از خوبیهای devise اینه که به شکل scaffold گونهای، میتونه به ما کمک کنه که کاربر و سیستم کنترلش رو بسازیم. برای ساخت مدل کاربر فقط کافیه که این دستور رو اجرا کنیم:
rails g devise User
به این شکل میفهمه که باید یک مدل، مطابق مدل User ولی با مشخصات devise برامون بسازه. بعدش هم کافیه این دستور رو اجرا کنیم:
rails db:migrate
که جدولای مرتبط برامون در دیتابیس ساخته بشن.
حالا که خیالمون از بابت این قضایا راحت شد چی؟ هیچی. دو تا کنترلر هم میسازیم به این شکل:
rails g controller users/sessions rails g controller users/registrations
بعد از این میشه گفت که کار ما اینجا تمام شده و باید بریم یه چیزایی رو ادیت کنیم 🙂
ویرایش مدل یوزر
بعد از این که کارهای بالا رو انجام دادیم، کافیه که بریم سراغ مدل یوزرمون و به این شکل ادیتش کنیم:
class User < ApplicationRecord devise :database_authenticatable, :jwt_authenticatable, :registerable, jwt_revocation_strategy: JwtDenylist end
حالا این کار برای چیه؟ برای اینه که ما یک جدول دیگر به اسم JWT Deny List در نظر میگیریم و توکنهای منقضیشده رو درونش قرار میدیم. به اون شکل وقتی توکنی منقضی بشه، میتونیم به کاربر خطا نشون بدیم یا از توسعهدهندههای فرانت تیم بخوایم که وقتی اون خطا رو دیدن، کاربر رو لاگ اوت کنن. خلاصه که راه برای رسیدن به نتیجه مطلوب زیاده. بگذریم، بعد از این، در پوشه مدلها لازمه که یک فایل به اسم jwt_denylist.rb ایجاد کنیم و این محتوا رو درونش قرار بدیم:
class JwtDenylist < ApplicationRecord include Devise::JWT::RevocationStrategies::Denylist self.table_name = 'jwt_denylist' end
بعد نیاز داریم که برای این قضیه یک مایگرشن اضافه کنیم:
rails g migration CreateJwtDenylist
سپس، فایل مایگرشن که معمولا در آدرس:
db/migrate
قرار داره رو باز میکنیم و محتواش رو به این شکل تغییر میدیم:
class CreateJwtDenylist < ActiveRecord::Migration[6.1] def change create_table :jwt_denylist do |t| t.string :jti, null: false t.datetime :exp, null: false end add_index :jwt_denylist, :jti end end
و بعد یک دور مایگرشنها رو اجرا میکنیم:
rails db:migrate
تا اینجا مطلب طولانی شد؟ ایرادی نداره. بریم یک قهوه بزنیم به بدن و برگردیم 🙂
کنترلر Session
امیدوارم که کافئین به قدر کافی مودتون رو بالا آورده باشه 🙂 حالا وقتشه که بریم و کنترلر session رو درست کنیم. نیازی نیست واقعا کار خاصی کنیم. تنها کاری که نیازه بکنیم اینه که کنترلری که ساختیم رو باز کنیم و این موارد رو درش کپی کنیم:
class Users::SessionsController < Devise::SessionsController respond_to :json private def respond_with(resource, _opts = {}) render json: { message: 'You are logged in.' }, status: :ok end def respond_to_on_destroy log_out_success && return if current_user log_out_failure end def log_out_success render json: { message: "You are logged out." }, status: :ok end def log_out_failure render json: { message: "Hmm nothing happened."}, status: :unauthorized end end
نکته مهم، اگر هنگام ساخت کنترلر، جای users از چیز دیگری استفاده کردید باید Users رو در کد بالا به اون تغییر بدید. اگر هم کلا چیزی نذاشتید، کل قسمت Users:: رو ازش حذف کنید.
کنترلر Registration
خب عین همون بخش قبلی، شما کافیه کنترلر registrations رو باز کنید و این کد رو درونش کپی کنید:
class Users::RegistrationsController < Devise::RegistrationsController respond_to :json private def respond_with(resource, _opts = {}) register_success && return if resource.persisted? register_failed end def register_success render json: { message: 'Signed up sucessfully.' } end def register_failed render json: { message: "Something went wrong." } end end
تنظیمات نهایی devise
خب اول در ترمینال (یا cmd) این دستور رو اجرا کنید:
rake secret
و یک کد طولانی و مسخره بهتون میده 😁 اون رو در فایل:
config/initializers/devise.rb
در آخر فایل به این شکل کپی کنید:
config.jwt do |jwt| jwt.secret = rake_secret_output end
نکته بسیار مهم اینجا چیه؟ این که حواستون باشه این صرفا یک پروژه تسته و برای محیط پروداکشن اصلا جالب نیست که سیکرتها و توکنها، هاردکد باشن. برای اون زمان میتونید از ENV استفاده کنید.
مسیرها
خب، الان که تقریبا همهچی آرومه و ما چقدر خوشحالیم، کافیه که بیاییم و فایل routes.rb رو هم به این شکل ویرایش کنیم:
Rails.application.routes.draw do devise_for :users, controllers: { sessions: 'users/sessions', registrations: 'users/registrations' } end
خب پس چی میمونه که انجام ندادیم؟ یک سری آزمایش ساده 🙂
ساخت کاربر
خب الان کافیه بعد اجرای سرور ریلز (مطابق آموزشهای قبلی)، این دستور رو اجرا کنیم:
curl -X POST -H "Content-type: application/json" -d '{ "user": { "email":"test@test.com", "password":"12345678", "password_confirmation":"12345678"}}' http://localhost:3000/users
بعد از اجرای این دستور، یک شیء جیسون ساده به این شکل:
{"message":"Signed up sucessfully."}
به ما برمیگرده.
دریافت توکن
حالا چطور توکن دریافت کنیم؟ این هم سادهست. فقط کافیه که این دستور رو اجرا کنیم:
curl -X POST -i -H "Content-type: application/json" -d '{"user":{"email":"test@test.com", "password":"12345678"}}' http://localhost:3000/users/sign_in
بعد در خروجی اول به ما چنین چیزی میده:
HTTP/1.1 200 OK X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff X-Download-Options: noopen X-Permitted-Cross-Domain-Policies: none Referrer-Policy: strict-origin-when-cross-origin Content-Type: application/json; charset=utf-8 Vary: Accept, Origin Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyIiwic2NwIjoidXNlciIsImF1ZCI6bnVsbCwiaWF0IjoxNjIyMTAyOTUxLCJleHAiOjE2MjIxMDY1NTEsImp0aSI6IjBlZDk0YTFmLTgzYTEtNDM3Yy1hOTBkLWQyNTNjY2ZlZDE5YyJ9.NohABuynT-F2GqfCscGtSF6zzK0iAVUIlajSqmMgIMA ETag: W/"36cb9f6def08029b9c6bff3c14a98017" Cache-Control: max-age=0, private, must-revalidate X-Request-Id: 0584c92c-5100-4acf-8a21-852faa1c0173 X-Runtime: 0.209443 Transfer-Encoding: chunked
که اینها سرایند (Header) های ما هستند. در این قسمت، هرچی جلوی Authorization قرار داره توکن ماست. و میتونیم ازش استفاده کنیم.
بخش بعدی هم اینه :
{"message":"You are logged in."}
که صرفا به ما میگه ورودمون موفقیتآمیز بوده.
فکر کنم این مطلب، آخرین مطلبی بود که در مورد بیسیکهای روبیآنریلز مینوشتم. احتمالا در آینده نهچندان دور، همه اینها رو با هم به یک سری آموزش ویدئویی تبدیل کنم و از طریق آپارات یا یوتوب منتشرشون کنم.
طبیعتا یک قسمتهایی از آموزش در این مطلب پوشش داده نشده، سعی میکنم در آینده یک یا دو پست تکمیلی هم ارائه کنم که همه این قضایا به خوبی پوشش داده بشه (یا این که کلا در بخش ویدئویی در خدمتتون باشم).
به صورت کلی، دوست دارم بدونم نظر شما در مورد این تیپ آموزشها چیه؟ آیا ادامهشون بدم یا خیر؟ و این که آیا پایهش هستید که بحث فرانتند رو هم شروع کنیم یا روی همین بکند باقی بمونیم و اول یه پروژه کامل رو بکندش رو بزنیم و بعد بریم سراغ فرانت؟ 🙂
در آخر هم بابت وقتی که گذاشتید و مطلب رو خوندید ازتون متشکرم.