Let's Go استفاده از زمینه درخواست › زمینه درخواست برای احراز هویت و مجوزدهی
قبلی · فهرست · بعدی
فصل 11.2.

زمینه درخواست برای احراز هویت و مجوزدهی (Request Context for Authentication and Authorization)

در این بخش، نحوه استفاده از زمینه درخواست (Request Context) برای احراز هویت (Authentication) و مجوزدهی (Authorization) را بررسی می‌کنیم. این شامل ذخیره‌سازی اطلاعات کاربر (User Information Storage) و بررسی دسترسی‌ها (Access Checks) می‌شود.

بنابراین، با توضیحات فوق، بیایید از قابلیت زمینه درخواست در برنامه خود استفاده کنیم.

ما با بازگشت به فایل internal/models/users.go و تکمیل متد UserModel.Exists() شروع می‌کنیم، به طوری که اگر کاربری با شناسه خاصی در جدول users وجود داشته باشد، true برگرداند و در غیر این صورت false. به این صورت:

File: internal/models/users.go
package models

...

func (m *UserModel) Exists(id int) (bool, error) {
    var exists bool

    stmt := "SELECT EXISTS(SELECT true FROM users WHERE id = ?)"

    err := m.DB.QueryRow(stmt, id).Scan(&exists)
    return exists, err
}

سپس بیایید یک فایل جدید cmd/web/context.go ایجاد کنیم. در این فایل، یک نوع سفارشی contextKey و یک متغیر isAuthenticatedContextKey تعریف می‌کنیم، به طوری که یک کلید منحصر به فرد برای ذخیره و بازیابی وضعیت احراز هویت از زمینه درخواست داشته باشیم (بدون خطر برخورد نام).

$ touch cmd/web/context.go
File: cmd/web/context.go
package main

type contextKey string

const isAuthenticatedContextKey = contextKey("isAuthenticated")

و اکنون برای بخش هیجان‌انگیز. بیایید یک متد میان‌افزار جدید authenticate() ایجاد کنیم که:

  1. شناسه کاربر را از داده‌های جلسه او بازیابی می‌کند.
  2. پایگاه داده را بررسی می‌کند تا ببیند آیا شناسه با یک کاربر معتبر مطابقت دارد یا خیر، با استفاده از متد UserModel.Exists().
  3. زمینه درخواست را به‌روزرسانی می‌کند تا شامل یک کلید isAuthenticatedContextKey با مقدار true باشد.

در اینجا کد آمده است:

File: cmd/web/middleware.go
package main

import (
    "context" // New import
    "fmt"
    "net/http"

    "github.com/justinas/nosurf"
)

...

func (app *application) authenticate(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Retrieve the authenticatedUserID value from the session using the
        // GetInt() method. This will return the zero value for an int (0) if no
        // "authenticatedUserID" value is in the session -- in which case we
        // call the next handler in the chain as normal and return.
        id := app.sessionManager.GetInt(r.Context(), "authenticatedUserID")
        if id == 0 {
            next.ServeHTTP(w, r)
            return
        }

        // Otherwise, we check to see if a user with that ID exists in our
        // database.
        exists, err := app.users.Exists(id)
        if err != nil {
            app.serverError(w, r, err)
            return
        }

        // If a matching user is found, we know that the request is
        // coming from an authenticated user who exists in our database. We
        // create a new copy of the request (with an isAuthenticatedContextKey
        // value of true in the request context) and assign it to r.
        if exists {
            ctx := context.WithValue(r.Context(), isAuthenticatedContextKey, true)
            r = r.WithContext(ctx)
        }

        // Call the next handler in the chain.
        next.ServeHTTP(w, r)
    })
}

نکته مهمی که باید در اینجا تأکید کرد، تفاوت زیر است:

خوب، بیایید فایل cmd/web/routes.go را به‌روزرسانی کنیم تا میان‌افزار authenticate() را در زنجیره میان‌افزار dynamic قرار دهیم:

File: cmd/web/routes.go
package main

...

func (app *application) routes() http.Handler {
     mux := http.NewServeMux()

    fileServer := http.FileServer(http.Dir("./ui/static/"))
    mux.Handle("GET /static/", http.StripPrefix("/static", fileServer))

     // Add the authenticate() middleware to the chain.
    dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate)

    mux.Handle("GET /{$}", dynamic.ThenFunc(app.home))
    mux.Handle("GET /snippet/view/{id}", dynamic.ThenFunc(app.snippetView))
    mux.Handle("GET /user/signup", dynamic.ThenFunc(app.userSignup))
    mux.Handle("POST /user/signup", dynamic.ThenFunc(app.userSignupPost))
    mux.Handle("GET /user/login", dynamic.ThenFunc(app.userLogin))
    mux.Handle("POST /user/login", dynamic.ThenFunc(app.userLoginPost))

    protected := dynamic.Append(app.requireAuthentication)

    mux.Handle("GET /snippet/create", protected.ThenFunc(app.snippetCreate))
    mux.Handle("POST /snippet/create", protected.ThenFunc(app.snippetCreatePost))
    mux.Handle("POST /user/logout", protected.ThenFunc(app.userLogoutPost))

    standard := alice.New(app.recoverPanic, app.logRequest, commonHeaders)
    return standard.Then(mux)
}

آخرین کاری که باید انجام دهیم این است که کمک‌کننده isAuthenticated() خود را به‌روزرسانی کنیم، به طوری که به جای بررسی داده‌های جلسه، اکنون زمینه درخواست را بررسی کند تا تعیین کند که آیا کاربر احراز هویت شده است یا خیر.

می‌توانیم این کار را به این صورت انجام دهیم:

File: cmd/web/helpers.go
package main

...

func (app *application) isAuthenticated(r *http.Request) bool {
    isAuthenticated, ok := r.Context().Value(isAuthenticatedContextKey).(bool)
    if !ok {
        return false
    }

    return isAuthenticated
}

نکته مهمی که باید در اینجا اشاره کرد این است که اگر در زمینه درخواست با کلید isAuthenticatedContextKey مقداری وجود نداشته باشد، یا مقدار زیرین یک bool نباشد، این تأیید نوع شکست خواهد خورد. در این صورت، ما یک بازگشت ایمن انجام می‌دهیم و false برمی‌گردانیم (یعنی فرض می‌کنیم که کاربر احراز هویت نشده است).

اگر دوست دارید، برنامه را دوباره اجرا کنید. باید به درستی کامپایل شود و اگر به عنوان یک کاربر خاص وارد شوید و در برنامه مرور کنید، باید دقیقاً مانند قبل کار کند.

سپس، اگر می‌خواهید، MySQL را باز کنید و رکورد کاربری که به عنوان آن وارد شده‌اید را از پایگاه داده حذف کنید. برای مثال:

mysql> USE snippetbox;
mysql> DELETE FROM users WHERE email = 'bob@example.com';

و وقتی به مرورگر خود برگردید و صفحه را تازه کنید، برنامه اکنون به اندازه کافی هوشمند است که تشخیص دهد کاربر حذف شده است و شما به عنوان یک کاربر غیرمعتبر (خارج شده) رفتار خواهید شد.


اطلاعات اضافی

استفاده نادرست از زمینه درخواست

مهم است که تأکید کنیم که زمینه درخواست باید فقط برای ذخیره اطلاعات مربوط به طول عمر یک درخواست خاص استفاده شود. مستندات Go برای context.Context هشدار می‌دهد:

از مقادیر زمینه فقط برای داده‌های محدود به درخواست که از فرآیندها و API‌ها عبور می‌کنند، استفاده کنید.

این بدان معناست که نباید از آن برای انتقال وابستگی‌هایی که خارج از طول عمر یک درخواست وجود دارند — مانند لاگرها، کش‌های قالب و استخر اتصال پایگاه داده شما — به میان‌افزار و هندلرهای خود استفاده کنید.

به دلایل ایمنی نوع و وضوح کد، تقریباً همیشه بهتر است این وابستگی‌ها را به‌طور صریح در دسترس هندلرهای خود قرار دهید، با این که یا هندلرهای خود را به عنوان متدهایی در برابر یک ساختار application (مانند آنچه در این کتاب داریم) یا با انتقال آنها در یک بسته (مانند این Gist) قرار دهید.

واژه‌نامه اصطلاحات فنی

اصطلاح فارسی معادل انگلیسی توضیح
زمینه درخواست Request Context محیط اجرای درخواست
احراز هویت Authentication تأیید هویت کاربر
مجوزدهی Authorization کنترل دسترسی کاربر
ذخیره‌سازی اطلاعات کاربر User Information Storage نگهداری داده‌های کاربر
بررسی دسترسی‌ها Access Checks کنترل مجوزهای دسترسی
میان‌افزار احراز هویت Authentication Middleware کد واسط برای تأیید هویت
کلید زمینه Context Key کلید ذخیره داده در زمینه
مدل کاربر User Model ساختار داده‌ای کاربر
نشست کاربر User Session جلسه کاری کاربر
کنترل دسترسی Access Control مدیریت مجوزها