احراز هویت JWT در روبی آن ریلز

پس از مدت طولانی، با یک پست دیگر برگشتم و این بار قراره با هم احراز هویت JWT رو در ریلز بررسی کنیم. اول از همه، لازمه بدونیم JWT چیه؟ چرا نیازش داریم؟ اصلا چرا احراز هویت و هزاران چرای دیگر که احتمالا الان در سر شما هستند. بعدش یک پروژه خیلی کوچولو ایجاد می‌کنیم و با هم براش احراز هویت رو پیاده‌سازی می‌کنیم 🙂

احراز هویت JWT چیه و چرا بهش نیاز داریم؟

اول از همه این سوال بنیادی‌تر رو پاسخ بدیم که «چرا احراز هویت نیاز داریم؟» و بعد بریم سراغ احراز هویت JWT که قراره در این مطلب در موردش مفصل حرف بزنیم.

ما به احراز هویت نیاز داریم چون همیشه بخشی از داده‌های ما، خصوصی هستند. گذشته از اون، احراز هویت می‌تونه اجازه CRUD رو به شما بده، نه؟ فکر کنید اپی دارید که هرکسی می‌تونه روش بخونه و بنویسه. ممکنه خوندن چیزی باشه که برای «همه» مناسب باشه اما «نوشتن» اینطور نیست. بخصوص که نوشتن خودش به قسمت‌هایی مثل حذف و بروزرسانی هم شکسته شده.

پس ما به احراز هویت نیاز داریم که هر ننه‌قمری (😂) نتونه از API ما استفاده کنه. بلکه کاربرانی که ثبت‌نام کردند و دسترسی درستی به سرویس دارند، بتونن استفاده کنن. این قضیه در API های تجاری (یا Business facing) خودشون رو بیشتر و بهتر نشون میدن.

حالا سوال مهم‌تر …

احراز هویت JWT چیه؟

در اپهای قدیمی (مثل همین وردپرس)، احراز هویت توسط cookie ها انجام میشه. به چه صورتی؟ به این صورت که وقتی نام کاربری و گذرواژه وارد می‌کنیم، نرم‌افزار فضایی رو در مرورگر به خودش اختصاص میده و یک سری اطلاعات رو با خودش جابجا می‌کنه. اما در ReST API ها این قضیه رو نداریم. در واقع نمی‌تونیم به کوکی‌ها اعتماد کنیم. پس چه می‌کنیم؟ اینجا لازمه جز یوزرنیم(که معمولا می‌تونه عمومی باشه) و پسورد (که می‌تونه راحت لو بره) میاییم و یک «توکن» هم تعریف می‌کنیم. این توکن، می‌تونه ثابت یا متغیر باشه. یعنی چی؟ یعنی می‌تونه به ازای هربار ورود تغییر کنه، می‌تونه سر زمان مشخصی هم منقضی بشه.

حالا توکن چیه؟ توکن به صورت کلی، در کازینوها معادل پولیه که شما در بازی‌ها قرار می‌دید، در واقع مجوز حضور شما در اون کازینو، کلاب و … است. حتی به رمزارزهایی که بر مبنای دیگر رمزارزها ساخته میشن هم سکه نمی‌گن بلکه میگن توکن. شما فرض کنید که مثلا ۱۰۰ دلار می‌دید و پنج تا سکه با آرم اون کازینوی خاص دریافت می‌کنید. اگر در بازی برنده بشید یا ببازید، باید توکن‌هاتون رو تحویل بدید یا بگیرید.

حالا در احراز هویت JWT هم، ما به ازای کاربرمون یک توکن در نظر می‌گیریم. این توکن، معمولا یک رشته طولانیه که انسان نمی‌تونه بخوندش. نتیجتا خیلی از اطلاعات ما به صورت ایمن‌تر می‌تونن رد و بدل بشن (طبیعیه که مواردی مثل SSL داشتن و الگوریتم‌هایی که در ساخت توکن داشتیم هم مهمن). ضمن این که نام‌کاربری، ایمیل، رمزعبور و .. هم به همین سادگیا نمی‌تونن خونده بشن.

پس ما می‌آییم و یک دیتابیسی از توکن‌ها در کنار دیتابیسی از یوزرها میسازیم (البته درست‌ترش، جدوله!) و به ازای هر یوزر معمولا دوتا توکن در اون دیتابیس قرار می‌دیم. یکیش رو بهش میگیم «توکن دسترسی» یا Access Token و یکی رو می‌گیم «توکن بازنشانی» یا Refresh Token. توکن دسترسی، معمولا یک تاریخ انقضایی داره و بعد از اون با استفاده از توکن بازنشانی، می‌تونیم یکی جدید بگیریم. اما در آموزش امروز، صرفا میخوایم توکن دسترسی رو به دست بیاریم.

شمایی از کار JWT
احراز هویت JWT به این شکل کار می‌کنه

خب، الان که تقریبا همه‌چی رو می‌دونیم، بریم برای پیاده‌سازی.

پیاده‌سازی یک اپلیکیشن ریلز با JWT

خب در قدم اول، باید یک اپ ایجاد کنیم. این اپ رو به این شکل ایجاد می‌کنیم:

rails new devise-jwt --api

خب توضیح واضحات:

  • قسمت rails که واضحا فراخوانی نرم‌افزار rails در ترمینال ماست.
  • قسمت new در خواست برای ایجاد یک اپ جدیده.
  • قسمت devise-jwt اسم پروژه ماست. حالا چرا؟ چون قراره از یک لایبرری با همین اسم استفاده کنیم. بنابراین، پروژه رو اینطوری اسم گذاشتیم.
  • در قسمت آخر هم، به ریلز گفتیم که ما تو رو بخاطر API هات دوست داریم. ویو نیاز نیست.

بعد از چند ثانیه (و بسته به سرعت اینترنت دقیقه) اپ ما ساخته می‌شه. بعد لازمه که مرحله مرحله کارهایی رو انجام بدیم.

نصب لایبرری‌های مورد نیاز

خب، اول از همه با ویرایشگر متنی مورد علاقمون فایل Gemfile رو باز می‌کنیم و این خطوط رو بهش اضافه می‌کنیم :
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."}

که صرفا به ما می‌گه ورودمون موفقیت‌آمیز بوده.

فکر کنم این مطلب، آخرین مطلبی بود که در مورد بیسیک‌های روبی‌آن‌ریلز می‌نوشتم. احتمالا در آینده نه‌چندان دور، همه این‌ها رو با هم به یک سری آموزش ویدئویی تبدیل کنم و از طریق آپارات یا یوتوب منتشرشون کنم.

طبیعتا یک قسمت‌هایی از آموزش در این مطلب پوشش داده نشده، سعی می‌کنم در آینده یک یا دو پست تکمیلی هم ارائه کنم که همه این قضایا به خوبی پوشش داده بشه (یا این که کلا در بخش ویدئویی در خدمتتون باشم).

به صورت کلی، دوست دارم بدونم نظر شما در مورد این تیپ آموزش‌ها چیه؟ آیا ادامه‌شون بدم یا خیر؟ و این که آیا پایه‌ش هستید که بحث فرانتند رو هم شروع کنیم یا روی همین بکند باقی بمونیم و اول یه پروژه کامل رو بکندش رو بزنیم و بعد بریم سراغ فرانت؟ 🙂

در آخر هم بابت وقتی که گذاشتید و مطلب رو خوندید ازتون متشکرم.

Share

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *