احراز هویت درخواستها
اکنون که کلاینتهای ما راهی برای مبادله اطلاعات ورود خود با یک token احراز هویت دارند، بیایید بررسی کنیم چگونه میتوانیم از آن token برای احراز هویت آنها استفاده کنیم تا دقیقاً بدانیم درخواست از کدام کاربر میآید.
اساساً، وقتی یک کلاینت یک token احراز هویت دارد، از آن انتظار داریم که آن را در تمام درخواستهای بعدی خود در هدر Authorization قرار دهد، به این صورت:
Authorization: Bearer IEYZQUBEMPPAKPOAWTPV6YJ6RM
هنگامی که این درخواستها را دریافت میکنیم، از یک متد middleware جدید authenticate() برای اجرای منطق زیر استفاده خواهیم کرد:
- اگر token احراز هویت معتبر نباشد، یک پاسخ
401 Unauthorizedو یک پیام خطا برای کلاینت ارسال میکنیم تا بداند token او نادرست یا غیرمعتبر است. - اگر token احراز هویت معتبر باشد، جزئیات کاربر را جستجو کرده و به request context اضافه میکنیم.
- اگر هیچ هدر
Authorizationارسال نشده باشد، در عوض جزئیات یک کاربر ناشناس را به request context اضافه میکنیم.
ایجاد کاربر ناشناس
بیایید با آخرین نکته شروع کنیم و ابتدا یک کاربر ناشناس را در فایل internal/data/users.go خود تعریف کنیم، به این صورت:
package data ... // Declare a new AnonymousUser variable. var AnonymousUser = &User{} type User struct { ID int64 `json:"id"` CreatedAt time.Time `json:"created_at"` Name string `json:"name"` Email string `json:"email"` Password password `json:"-"` Activated bool `json:"activated"` Version int `json:"-"` } // Check if a User instance is the AnonymousUser. func (u *User) IsAnonymous() bool { return u == AnonymousUser } ...
بنابراین در اینجا یک متغیر جدید AnonymousUser ایجاد کردهایم که یک اشارهگر به یک struct User را نگهداری میکند که نشاندهنده یک کاربر غیرفعال با شناسه، نام، ایمیل یا رمز عبور است.
همچنین یک متد IsAnonymous() روی struct User پیادهسازی کردهایم تا هر زمان که یک نمونه User داریم بتوانیم به راحتی بررسی کنیم آیا نمونه AnonymousUser است یا خیر. به عنوان مثال:
data.AnonymousUser.IsAnonymous() // → Returns true otherUser := &data.User{} otherUser.IsAnonymous() // → Returns false
خواندن و نوشتن در request context
مرحله تنظیم دیگر، قبل از اینکه به ایجاد خود middleware authenticate() بپردازیم، مربوط به ذخیره جزئیات کاربر در request context است.
ما در کتاب Let's Go به طور مفصل درباره اینکه request context چیست و چگونه از آن استفاده کنیم بحث کردیم و اگر هر قسمتی ناآشنا به نظر میرسد، توصیه میکنم قبل از ادامه آن بخش کتاب را مجدداً بخوانید. اما به عنوان یک یادآوری سریع:
هر
http.Requestکه برنامه ما پردازش میکند دارای یکcontext.Contextتعبیه شده است که میتوانیم از آن برای ذخیره جفتهای کلید-مقدار حاوی دادههای دلخواه در طول عمر درخواست استفاده کنیم. در این مورد میخواهیم یک structUserحاوی اطلاعات کاربر فعلی ذخیره کنیم.هر مقداری که در request context ذخیره میشود نوع
anyدارد. این بدان معناست که پس از بازیابی یک مقدار از request context باید آن را به نوع اصلی خود برگردانید قبل از استفاده از آن.بهتر است از نوع سفارشی خود برای کلیدهای request context استفاده کنید. این به جلوگیری از تداخل نامها بین کد شما و هر بسته شخص ثالثی که از request context برای ذخیره اطلاعات استفاده میکند کمک میکند.
برای کمک به این کار، بیایید یک فایل جدید cmd/api/context.go ایجاد کنیم حاوی برخی متدهای کمکی برای خواندن/نوشتن struct User در request context و از آن.
اگر در حال دنبال کردن هستید، فایل جدید را ایجاد کنید:
$ touch cmd/api/context.go
و سپس کد زیر را اضافه کنید:
package main import ( "context" "net/http" "greenlight.alexedwards.net/internal/data" ) // Define a custom contextKey type, with the underlying type string. type contextKey string // Convert the string "user" to a contextKey type and assign it to the userContextKey // constant. We'll use this constant as the key for getting and setting user information // in the request context. const userContextKey = contextKey("user") // The contextSetUser() method returns a new copy of the request with the provided // User struct added to the context. Note that we use our userContextKey constant as the // key. func (app *application) contextSetUser(r *http.Request, user *data.User) *http.Request { ctx := context.WithValue(r.Context(), userContextKey, user) return r.WithContext(ctx) } // The contextGetUser() retrieves the User struct from the request context. The only // time that we'll use this helper is when we logically expect there to be User struct // value in the context, and if it doesn't exist it will firmly be a programmer error. // As we discussed earlier in the book, it's OK to panic in those circumstances. func (app *application) contextGetUser(r *http.Request) *data.User { user, ok := r.Context().Value(userContextKey).(*data.User) if !ok { panic("missing user value in request context") } return user }
ایجاد middleware احراز هویت
اکنون که این موارد را آماده کردهایم، آمادهایم کار روی خود middleware authenticate() را شروع کنیم.
فایل cmd/api/middleware.go خود را باز کنید و کد زیر را اضافه کنید:
package main import ( "errors" // New import "fmt" "net" "net/http" "strings" // New import "sync" "time" "greenlight.alexedwards.net/internal/data" // New import "greenlight.alexedwards.net/internal/validator" // New import "golang.org/x/time/rate" ) ... func (app *application) authenticate(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Add the "Vary: Authorization" header to the response. This indicates to any // caches that the response may vary based on the value of the Authorization // header in the request. w.Header().Add("Vary", "Authorization") // Retrieve the value of the Authorization header from the request. This will // return the empty string "" if there is no such header found. authorizationHeader := r.Header.Get("Authorization") // If there is no Authorization header found, use the contextSetUser() helper // that we just made to add the AnonymousUser to the request context. Then we // call the next handler in the chain and return without executing any of the // code below. if authorizationHeader == "" { r = app.contextSetUser(r, data.AnonymousUser) next.ServeHTTP(w, r) return } // Otherwise, we expect the value of the Authorization header to be in the format // "Bearer <token>". We try to split this into its constituent parts, and if the // header isn't in the expected format we return a 401 Unauthorized response // using the invalidAuthenticationTokenResponse() helper (which we will create // in a moment). headerParts := strings.Split(authorizationHeader, " ") if len(headerParts) != 2 || headerParts[0] != "Bearer" { app.invalidAuthenticationTokenResponse(w, r) return } // Extract the actual authentication token from the header parts. token := headerParts[1] // Validate the token to make sure it is in a sensible format. v := validator.New() // If the token isn't valid, use the invalidAuthenticationTokenResponse() // helper to send a response, rather than the failedValidationResponse() helper // that we'd normally use. if data.ValidateTokenPlaintext(v, token); !v.Valid() { app.invalidAuthenticationTokenResponse(w, r) return } // Retrieve the details of the user associated with the authentication token, // again calling the invalidAuthenticationTokenResponse() helper if no // matching record was found. IMPORTANT: Notice that we are using // ScopeAuthentication as the first parameter here. user, err := app.models.Users.GetForToken(data.ScopeAuthentication, token) if err != nil { switch { case errors.Is(err, data.ErrRecordNotFound): app.invalidAuthenticationTokenResponse(w, r) default: app.serverErrorResponse(w, r, err) } return } // Call the contextSetUser() helper to add the user information to the request // context. r = app.contextSetUser(r, user) // Call the next handler in the chain. next.ServeHTTP(w, r) }) }
کد زیادی در اینجا وجود دارد، بنابراین برای کمک به روشن شدن اوضاع، بیایید به سرعت اعمالی که این middleware انجام میدهد را مرور کنیم:
- اگر یک token احراز هویت معتبر در هدر
Authorizationارائه شده باشد، یک structUserحاوی جزئیات کاربر مربوطه در request context ذخیره خواهد شد. - اگر هیچ هدر
Authorizationارائه نشده باشد، structAnonymousUserما در request context ذخیره خواهد شد. - اگر هدر
Authorizationارائه شده باشد، اما نادرست باشد یا مقدار غیرمعتبری داشته باشد، یک پاسخ401 Unauthorizedبا استفاده از متد کمکیinvalidAuthenticationTokenResponse()برای کلاینت ارسال خواهد شد.
صحبت از آن شد، بیایید به فایل cmd/api/errors.go خود برویم و آن متد کمکی را به صورت زیر ایجاد کنیم:
package main ... func (app *application) invalidAuthenticationTokenResponse(w http.ResponseWriter, r *http.Request) { w.Header().Set("WWW-Authenticate", "Bearer") message := "invalid or missing authentication token" app.errorResponse(w, r, http.StatusUnauthorized, message) }
در نهایت، باید middleware authenticate() را به زنجیره handler خود اضافه کنیم. میخواهیم از این middleware روی تمام درخواستها استفاده کنیم — بعد از middleware بازیابی panic و rate limiter، اما قبل از router.
فایل cmd/api/routes.go خود را مطابق با آن بهروزرسانی کنید:
package main ... func (app *application) routes() http.Handler { router := httprouter.New() router.NotFound = http.HandlerFunc(app.notFoundResponse) router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse) router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler) router.HandlerFunc(http.MethodGet, "/v1/movies", app.listMoviesHandler) router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler) router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler) router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler) router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler) router.HandlerFunc(http.MethodPost, "/v1/users", app.registerUserHandler) router.HandlerFunc(http.MethodPut, "/v1/users/activated", app.activateUserHandler) router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler) // Use the authenticate() middleware on all requests. return app.recoverPanic(app.rateLimit(app.authenticate(router))) }
نمایش
بیایید این را با ارسال یک درخواست بدون هدر Authorization تست کنیم. در پشت صحنه، middleware authenticate() ما AnonymousUser را به request context اضافه خواهد کرد و درخواست باید با موفقیت تکمیل شود. به این صورت:
$ curl localhost:4000/v1/healthcheck
{
"status": "available",
"system_info": {
"environment": "development",
"version": "1.0.0"
}
}
سپس بیایید همان کار را امتحان کنیم، اما با یک token احراز هویت معتبر در هدر Authorization. این بار، جزئیات کاربر مربوطه باید به request context اضافه شود و باید دوباره یک پاسخ موفق دریافت کنیم. به عنوان مثال:
$ curl -d '{"email": "alice@example.com", "password": "pa55word"}' localhost:4000/v1/tokens/authentication
{
"authentication_token": {
"token": "FXCZM44TVLC6ML2NXTOW5OHFUE",
"expiry": "2021-04-17T12:20:30.02833444+02:00"
}
}
$ curl -H "Authorization: Bearer FXCZM44TVLC6ML2NXTOW5OHFUE" localhost:4000/v1/healthcheck
{
"status": "available",
"system_info": {
"environment": "development",
"version": "1.0.0"
}
}
در مقابل، میتوانید ارسال برخی درخواستها با یک token احراز هویت غیرمعتبر یا یک هدر Authorization نادرست را نیز امتحان کنید. در این موارد باید یک پاسخ 401 Unauthorized دریافت کنید، به این صورت:
$ curl -i -H "Authorization: Bearer XXXXXXXXXXXXXXXXXXXXXXXXXX" localhost:4000/v1/healthcheck
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Vary: Authorization
Www-Authenticate: Bearer
Date: Fri, 16 Apr 2021 10:23:06 GMT
Content-Length: 56
{
"error": "invalid or missing authentication token"
}
$ curl -i -H "Authorization: INVALID" localhost:4000/v1/healthcheck
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Vary: Authorization
Www-Authenticate: Bearer
Date: Fri, 16 Apr 2021 10:23:26 GMT
Content-Length: 56
{
"error": "invalid or missing authentication token"
}