زمینه درخواست برای احراز هویت و مجوزدهی (Request Context for Authentication and Authorization)
در این بخش، نحوه استفاده از زمینه درخواست (Request Context) برای احراز هویت (Authentication) و مجوزدهی (Authorization) را بررسی میکنیم. این شامل ذخیرهسازی اطلاعات کاربر (User Information Storage) و بررسی دسترسیها (Access Checks) میشود.
بنابراین، با توضیحات فوق، بیایید از قابلیت زمینه درخواست در برنامه خود استفاده کنیم.
ما با بازگشت به فایل internal/models/users.go و تکمیل متد UserModel.Exists() شروع میکنیم، به طوری که اگر کاربری با شناسه خاصی در جدول users وجود داشته باشد، true برگرداند و در غیر این صورت false. به این صورت:
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
package main type contextKey string const isAuthenticatedContextKey = contextKey("isAuthenticated")
و اکنون برای بخش هیجانانگیز. بیایید یک متد میانافزار جدید authenticate() ایجاد کنیم که:
- شناسه کاربر را از دادههای جلسه او بازیابی میکند.
- پایگاه داده را بررسی میکند تا ببیند آیا شناسه با یک کاربر معتبر مطابقت دارد یا خیر، با استفاده از متد
UserModel.Exists(). - زمینه درخواست را بهروزرسانی میکند تا شامل یک کلید
isAuthenticatedContextKeyبا مقدارtrueباشد.
در اینجا کد آمده است:
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) }) }
نکته مهمی که باید در اینجا تأکید کرد، تفاوت زیر است:
- وقتی کاربر معتبر نداریم، درخواست اصلی و بدون تغییر
*http.Requestرا به هندلر بعدی در زنجیره میفرستیم. - وقتی کاربر معتبر داریم، یک نسخه از درخواست با یک کلید
isAuthenticatedContextKeyو مقدارtrueدر زمینه درخواست ایجاد میکنیم. سپس این نسخه از*http.Requestرا به هندلر بعدی در زنجیره میفرستیم.
خوب، بیایید فایل cmd/web/routes.go را بهروزرسانی کنیم تا میانافزار authenticate() را در زنجیره میانافزار dynamic قرار دهیم:
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() خود را بهروزرسانی کنیم، به طوری که به جای بررسی دادههای جلسه، اکنون زمینه درخواست را بررسی کند تا تعیین کند که آیا کاربر احراز هویت شده است یا خیر.
میتوانیم این کار را به این صورت انجام دهیم:
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 | مدیریت مجوزها |