Let's Go Further مجوز مبتنی بر نقش › نیاز به فعال‌سازی کاربر
قبلی · فهرست مطالب · بعدی
فصل ۱۶.۱.

نیاز به فعال‌سازی کاربر

همان‌طور که چند لحظه پیش ذکر کردیم، اولین کاری که از نظر authorization انجام خواهیم داد، محدود کردن دسترسی به endpointهای /v1/movies** است — به طوری که فقط کاربرانی که احراز هویت شده‌اند (ناشناس نیستند) و حساب خود را فعال کرده‌اند بتوانند به آن‌ها دسترسی داشته باشند.

انجام این نوع بررسی‌ها یک کار ایده‌آل برای middleware است، پس بیایید وارد شویم و یک متد middleware جدید requireActivatedUser() برای مدیریت این موضوع بسازیم. در این middleware، می‌خواهیم struct User را از request context استخراج کنیم و سپس متد IsAnonymous() و فیلد Activated را بررسی کنیم تا تعیین کنیم آیا request باید ادامه یابد یا خیر.

به طور خاص:

پس ابتدا، بیایید به فایل cmd/api/errors.go برویم و چند helper جدید برای ارسال این پیام‌های خطا اضافه کنیم. به این صورت:

فایل: cmd/api/errors.go
package main

...

func (app *application) authenticationRequiredResponse(w http.ResponseWriter, r *http.Request) {
    message := "you must be authenticated to access this resource"
    app.errorResponse(w, r, http.StatusUnauthorized, message)
}

func (app *application) inactiveAccountResponse(w http.ResponseWriter, r *http.Request) {
    message := "your user account must be activated to access this resource"
    app.errorResponse(w, r, http.StatusForbidden, message)
}

و سپس بیایید middleware جدید requireActivatedUser() را برای انجام بررسی‌ها بسازیم. کدی که نیاز داریم ساده و مختصر است:

فایل: cmd/api/middleware.go
package main

...

func (app *application) requireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Use the contextGetUser() helper that we made earlier to retrieve the user 
        // information from the request context.
        user := app.contextGetUser(r)

        // If the user is anonymous, then call the authenticationRequiredResponse() to 
        // inform the client that they should authenticate before trying again.
        if user.IsAnonymous() {
            app.authenticationRequiredResponse(w, r)
            return
        }

        // If the user is not activated, use the inactiveAccountResponse() helper to 
        // inform them that they need to activate their account.
        if !user.Activated {
            app.inactiveAccountResponse(w, r)
            return
        }

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

توجه کنید که middleware requireActivatedUser() ما امضای کمی متفاوت نسبت به middlewareهای دیگری که در این کتاب ساخته‌ایم دارد. به جای پذیرش و بازگرداندن http.Handler، http.HandlerFunc را پذیرفته و بازمی‌گرداند.

این یک تغییر کوچک است، اما این امکان را فراهم می‌کند که توابع handler /v1/movie** خود را مستقیماً با این middleware پوشش دهیم، بدون نیاز به تبدیل‌های بیشتر.

فایل 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)

    // Use the requireActivatedUser() middleware on our five /v1/movies** endpoints.
    router.HandlerFunc(http.MethodGet, "/v1/movies", app.requireActivatedUser(app.listMoviesHandler))
    router.HandlerFunc(http.MethodPost, "/v1/movies", app.requireActivatedUser(app.createMovieHandler))
    router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.requireActivatedUser(app.showMovieHandler))
    router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.requireActivatedUser(app.updateMovieHandler))
    router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.requireActivatedUser(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)

    return app.recoverPanic(app.rateLimit(app.authenticate(router)))
}

نمایش

خب، بیایید این تغییرات را امتحان کنیم!

با فراخوانی endpoint GET /v1/movies/:id به عنوان یک کاربر ناشناس شروع می‌کنیم. با انجام این کار باید اکنون پاسخ 401 Unauthorized دریافت کنید، به این صورت:

$ curl -i localhost:4000/v1/movies/1
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Vary: Authorization
Www-Authenticate: Bearer
Date: Fri, 16 Apr 2021 15:59:33 GMT
Content-Length: 66

{
    "error": "you must be authenticated to access this resource"
}

سپس، بیایید سعی کنیم درخواستی به عنوان کاربری که حساب دارد اما هنوز فعال نشده ارسال کنیم. اگر همراه ما بوده‌اید، باید بتوانید از کاربر alice@example.com برای این کار استفاده کنید، به این صورت:

$ BODY='{"email": "alice@example.com", "password": "pa55word"}'
$ curl -d "$BODY" localhost:4000/v1/tokens/authentication
{
    "authentication_token": {
        "token": "2O4YHHWDHVVWWDNKN2UZR722BU",
        "expiry": "2021-04-17T18:03:09.598843181+02:00"
    }
}

$ curl -i -H "Authorization: Bearer 2O4YHHWDHVVWWDNKN2UZR722BU" localhost:4000/v1/movies/1
HTTP/1.1 403 Forbidden
Content-Type: application/json
Vary: Authorization
Date: Fri, 16 Apr 2021 16:03:45 GMT
Content-Length: 76

{
    "error": "your user account must be activated to access this resource"
}

عالی، می‌بینیم که این اکنون منجر به پاسخ 403 Forbidden از helper جدید inactiveAccountResponse() ما می‌شود.

در نهایت، بیایید سعی کنیم درخواستی به عنوان یک کاربر فعال ارسال کنیم.

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

$ psql $GREENLIGHT_DB_DSN 
psql (15.4 (Ubuntu 15.4-1.pgdg22.04+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

greenlight=> SELECT email FROM users WHERE activated = true;
       email       
-------------------
 faith@example.com
(1 row)

در مورد من، تنها کاربر فعال faith@example.com است، پس بیایید سعی کنیم درخواستی به عنوان او ارسال کنیم. هنگام ارسال درخواست به عنوان یک کاربر فعال، تمام بررسی‌های middleware requireActivatedUser() ما رد شده و باید پاسخ موفقی دریافت کنید. مشابه این:

$ BODY='{"email": "faith@example.com", "password": "pa55word"}'
$ curl -d "$BODY" localhost:4000/v1/tokens/authentication
{
    "authentication_token": {
        "token": "ZFIKQ344EYM5KEP6JL2RHLRPJQ",
        "expiry": "2021-04-17T18:04:57.513348573+02:00"
    }
}

$ curl -H "Authorization: Bearer ZFIKQ344EYM5KEP6JL2RHLRPJQ" localhost:4000/v1/movies/1
{
    "movie": {
        "id": 1,
        "title": "Moana",
        "year": 2016,
        "runtime": "107 mins",
        "genres": [
            "animation",
            "adventure"
        ],
        "version": 1
    }
}

تقسیم کردن middleware

در حال حاضر ما یک middleware داریم که دو بررسی انجام می‌دهد: ابتدا بررسی می‌کند که کاربر احراز هویت شده (ناشناس نیست) و سپس بررسی می‌کند که فعال شده است.

اما می‌توان سناریویی را تصور کرد که در آن فقط می‌خواهید بررسی کنید که آیا کاربر احراز هویت شده و به فعال بودن یا نبودن او اهمیتی نمی‌دهید. برای کمک به این موضوع، ممکن است بخواهید علاوه بر middleware فعلی requireActivatedUser()، یک middleware اضافی requireAuthenticatedUser() نیز معرفی کنید.

با این حال، بین این دو middleware همپوشانی وجود خواهد داشت، زیرا هر دو بررسی می‌کنند که آیا کاربر احراز هویت شده یا خیر. روشی تمیز برای جلوگیری از این تکرار این است که middleware requireActivatedUser() شما به طور خودکار middleware requireAuthenticatedUser() را فراخوانی کند.

توصیف این الگو با کلمات دشوار است، پس آن را نشان می‌دهم. اگر همراه ما هستید، فایل cmd/api/middleware.go خود را به این صورت به‌روز کنید:

فایل: cmd/api/middleware.go
package main

...

// Create a new requireAuthenticatedUser() middleware to check that a user is not 
// anonymous.
func (app *application) requireAuthenticatedUser(next http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user := app.contextGetUser(r)

        if user.IsAnonymous() {
            app.authenticationRequiredResponse(w, r)
            return
        }

        next.ServeHTTP(w, r)
    })
}

// Checks that a user is both authenticated and activated.
func (app *application) requireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
    // Rather than returning this http.HandlerFunc we assign it to the variable fn.
    fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user := app.contextGetUser(r)

        // Check that a user is activated.
        if !user.Activated {
            app.inactiveAccountResponse(w, r)
            return
        }

        next.ServeHTTP(w, r)
    })

    // Wrap fn with the requireAuthenticatedUser() middleware before returning it.
    return app.requireAuthenticatedUser(fn)
}

به روشی که این را تنظیم کرده‌ایم، middleware requireActivatedUser() ما اکنون به طور خودکار middleware requireAuthenticatedUser() را قبل از اجرای خودش فراخوانی می‌کند. در برنامه ما این منطقی است — نباید بررسی کنیم آیا کاربر فعال شده مگر اینکه دقیقاً بدانیم کیست!

اکنون می‌توانید برنامه را دوباره اجرا کنید — همه چیز باید کامپایل شده و دقیقاً مانند قبل کار کند.


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

بررسی‌های درون handler

اگر فقط چند endpoint دارید که می‌خواهید بررسی‌های authorization را روی آن‌ها انجام دهید، به جای استفاده از middleware معمولاً راحت‌تر است بررسی‌ها را داخل handlerهای مربوطه انجام دهید. به عنوان مثال:

func (app *application) exampleHandler(w http.ResponseWriter, r *http.Request) {
    user := app.contextGetUser(r)

    if user.IsAnonymous() {
        app.authenticationRequiredResponse(w, r)
        return
    }

    if !user.Activated {
        app.inactiveAccountResponse(w, r)
        return
    }
    
    // The rest of the handler logic goes here...
}