context درخواست برای احراز هویت و مجوزدهی
پس، با این توضیحات، بیایید شروع به استفاده از قابلیت context درخواست در برنامه خود کنیم.
با بازگشت به فایل internal/models/users.go خود شروع میکنیم و متد UserModel.Exists() را تکمیل میکنیم، تا اگر کاربری با ID مشخص در جدول 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 تعریف میکنیم، تا یک کلید یکتا داشته باشیم که میتوانیم از آن برای ذخیره و بازیابی وضعیت احراز هویت (authentication status) از یک context درخواست استفاده کنیم (بدون خطر برخورد نام (naming collisions)).
$ touch cmd/web/context.go
package main type contextKey string const isAuthenticatedContextKey = contextKey("isAuthenticated")
و حالا برای بخش هیجانانگیز. بیایید یک متد middleware جدید authenticate() ایجاد کنیم که:
- ID کاربر را از دادههای نشست آنها بازیابی میکند.
- پایگاه داده را بررسی میکند تا ببیند آیا ID با یک کاربر معتبر مطابقت دارد یا نه با استفاده از متد
UserModel.Exists(). - context درخواست را بهروزرسانی میکند تا یک کلید
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اصلی و بدون تغییر را به handler بعدی در زنجیره منتقل میکنیم. - وقتی یک کاربر احراز هویت شده معتبر داریم، یک کپی از درخواست با یک کلید
isAuthenticatedContextKeyو مقدارtrueذخیره شده در context درخواست ایجاد میکنیم. سپس این کپی از*http.Requestرا به handler بعدی در زنجیره منتقل میکنیم.
خوب، بیایید فایل cmd/web/routes.go را بهروزرسانی کنیم تا middleware authenticate() را در زنجیره middleware 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) }
آخرین کاری که باید انجام دهیم بهروزرسانی helper isAuthenticated() ما است، تا به جای بررسی دادههای نشست، اکنون context درخواست را بررسی کند تا تعیین کند که آیا یک کاربر احراز هویت شده است یا نه.
میتوانیم این کار را به این صورت انجام دهیم:
package main ... func (app *application) isAuthenticated(r *http.Request) bool { isAuthenticated, ok := r.Context().Value(isAuthenticatedContextKey).(bool) if !ok { return false } return isAuthenticated }
نکته مهمی که باید در اینجا اشاره کنم این است که اگر مقداری در context درخواست با کلید isAuthenticatedContextKey وجود نداشته باشد، یا مقدار زیرین یک bool نباشد، این تبدیل نوع ناموفق خواهد بود. در آن صورت ما یک بازگشت ‘امن’ میگیریم و false برمیگردانیم (یعنی فرض میکنیم که کاربر احراز هویت نشده است).
اگر میخواهید، دوباره برنامه را اجرا کنید. باید به درستی کامپایل شود و اگر به عنوان یک کاربر خاص وارد شوید و در برنامه گشتزنی کنید، باید دقیقاً مانند قبل کار کند.
سپس، اگر میخواهید، MySQL را باز کنید و رکورد کاربری که به عنوان آن وارد شدهاید را از پایگاه داده حذف کنید. به عنوان مثال:
mysql> USE snippetbox; mysql> DELETE FROM users WHERE email = 'bob@example.com';
و وقتی به مرورگر خود برمیگردید و صفحه را رفرش میکنید، برنامه اکنون به اندازه کافی هوشمند است که تشخیص دهد کاربر حذف شده است، و خود را به عنوان یک کاربر احراز هویت نشده (خروج کرده) میبینید.
اطلاعات اضافی
سوء استفاده از context درخواست
نکته مهمی که باید تأکید کنم این است که context درخواست باید فقط برای ذخیره اطلاعات مرتبط با طول عمر یک درخواست خاص استفاده شود. مستندات Go برای context.Context هشدار میدهد:
از context Values فقط برای دادههای محدود به درخواست که بین فرآیندها و APIها عبور میکنند استفاده کنید.
این یعنی نباید از آن برای انتقال وابستگیهایی که خارج از طول عمر یک درخواست وجود دارند — مانند loggerها، cacheهای template و connection pool پایگاه داده شما — به middleware و handlerهای خود استفاده کنید.
به دلایل امنیت نوع و وضوح کد، تقریباً همیشه بهتر است این وابستگیها را بهطور صریح در دسترس handlerهای خود قرار دهید، یا با تبدیل handlerهای خود به متدهایی در برابر یک struct application (مانند آنچه در این کتاب داریم) یا با انتقال آنها در یک closure (مانند این Gist).