Let's Go Further احراز هویت › احراز هویت درخواست‌ها
قبلی · فهرست مطالب · بعدی
فصل ۱۵.۳.

احراز هویت درخواست‌ها

اکنون که کلاینت‌های ما راهی برای مبادله اطلاعات ورود خود با یک token احراز هویت دارند، بیایید بررسی کنیم چگونه می‌توانیم از آن token برای احراز هویت آن‌ها استفاده کنیم تا دقیقاً بدانیم درخواست از کدام کاربر می‌آید.

اساساً، وقتی یک کلاینت یک token احراز هویت دارد، از آن انتظار داریم که آن را در تمام درخواست‌های بعدی خود در هدر Authorization قرار دهد، به این صورت:

Authorization: Bearer IEYZQUBEMPPAKPOAWTPV6YJ6RM

هنگامی که این درخواست‌ها را دریافت می‌کنیم، از یک متد middleware جدید authenticate() برای اجرای منطق زیر استفاده خواهیم کرد:

ایجاد کاربر ناشناس

بیایید با آخرین نکته شروع کنیم و ابتدا یک کاربر ناشناس را در فایل internal/data/users.go خود تعریف کنیم، به این صورت:

فایل: 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 چیست و چگونه از آن استفاده کنیم بحث کردیم و اگر هر قسمتی ناآشنا به نظر می‌رسد، توصیه می‌کنم قبل از ادامه آن بخش کتاب را مجدداً بخوانید. اما به عنوان یک یادآوری سریع:

برای کمک به این کار، بیایید یک فایل جدید cmd/api/context.go ایجاد کنیم حاوی برخی متدهای کمکی برای خواندن/نوشتن struct User در request context و از آن.

اگر در حال دنبال کردن هستید، فایل جدید را ایجاد کنید:

$ touch cmd/api/context.go

و سپس کد زیر را اضافه کنید:

فایل: 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 خود را باز کنید و کد زیر را اضافه کنید:

فایل: 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 انجام می‌دهد را مرور کنیم:

صحبت از آن شد، بیایید به فایل cmd/api/errors.go خود برویم و آن متد کمکی را به صورت زیر ایجاد کنیم:

فایل: 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 خود را مطابق با آن به‌روزرسانی کنید:

فایل: 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"
}