Let's Go احراز هویت کاربر › مجوز کاربر
قبلی · فهرست · بعدی
فصل 10.6.

مجوز کاربر (User Authorization)

در این بخش، نحوه پیاده‌سازی مجوز کاربر (User Authorization) را بررسی می‌کنیم. این شامل کنترل دسترسی (Access Control)، محافظت از مسیرها (Route Protection) و میان‌افزار احراز هویت (Authentication Middleware) می‌شود.

برای شروع، بیایید یک میان‌افزار (Middleware) برای بررسی مجوز (Authorization Check) ایجاد کنیم:

احراز هویت کاربران برنامه ما خوب است، اما اکنون باید با آن اطلاعات کاری مفید انجام دهیم. در این فصل، برخی از بررسی‌های مجوز را معرفی خواهیم کرد تا:

  1. فقط کاربران احراز هویت شده (یعنی وارد شده) بتوانند یک قطعه جدید ایجاد کنند؛ و
  2. محتوای نوار ناوبری بسته به اینکه کاربر احراز هویت شده است (وارد شده) یا نه تغییر کند. به طور خاص:
    • کاربران احراز هویت شده باید لینک‌های ‘خانه’، ‘ایجاد قطعه’ و ‘خروج’ را ببینند.
    • کاربران احراز هویت نشده باید لینک‌های ‘خانه’، ‘ثبت‌نام’ و ‘ورود’ را ببینند.

همانطور که در فصل قبلی به طور مختصر اشاره کردم، می‌توانیم بررسی کنیم که آیا درخواست توسط یک کاربر احراز هویت شده انجام شده است یا نه با بررسی وجود یک مقدار "authenticatedUserID" در داده‌های جلسه آنها.

بنابراین بیایید با آن شروع کنیم. فایل cmd/web/helpers.go را باز کنید و یک تابع کمکی isAuthenticated() اضافه کنید تا وضعیت احراز هویت را به این صورت برگرداند:

File: cmd/web/helpers.go
package main

...

// Return true if the current request is from an authenticated user, otherwise
// return false.
func (app *application) isAuthenticated(r *http.Request) bool {
    return app.sessionManager.Exists(r.Context(), "authenticatedUserID")
}

این عالی است. اکنون می‌توانیم بررسی کنیم که آیا درخواست از یک کاربر احراز هویت شده (وارد شده) است یا نه با فراخوانی ساده این تابع کمکی isAuthenticated().

گام بعدی این است که راهی پیدا کنیم تا این اطلاعات را به قالب‌های HTML خود منتقل کنیم، تا بتوانیم محتوای نوار ناوبری را به درستی تغییر دهیم.

دو بخش برای این کار وجود دارد. اول، باید یک فیلد جدید IsAuthenticated به ساختار templateData خود اضافه کنیم:

File: cmd/web/templates.go
package main

import (
    "html/template"
    "path/filepath"
    "time"

    "snippetbox.alexedwards.net/internal/models"
)

type templateData struct {
    CurrentYear     int
    Snippet         models.Snippet
    Snippets        []models.Snippet
    Form            any
    Flash           string
    IsAuthenticated bool // Add an IsAuthenticated field to the templateData struct.
}

...

و گام دوم این است که تابع کمکی newTemplateData() خود را به‌روزرسانی کنیم تا این اطلاعات به‌طور خودکار به ساختار templateData اضافه شود هر بار که یک قالب را رندر می‌کنیم. به این صورت:

File: cmd/web/helpers.go
package main

...

func (app *application) newTemplateData(r *http.Request) templateData {
    return templateData{
        CurrentYear:     time.Now().Year(),
        Flash:           app.sessionManager.PopString(r.Context(), "flash"),
        // Add the authentication status to the template data.
        IsAuthenticated: app.isAuthenticated(r),
    }
}

...

پس از انجام این کار، می‌توانیم فایل ui/html/partials/nav.tmpl را به‌روزرسانی کنیم تا لینک‌های ناوبری را با استفاده از عمل {{if .IsAuthenticated}} به این صورت تغییر دهیم:

File: ui/html/partials/nav.tmpl
{{define "nav"}}
<nav>
    <div>
        <a href='/'>Home</a>
        <!-- Toggle the link based on authentication status -->
        {{if .IsAuthenticated}}
            <a href='/snippet/create'>Create snippet</a>
        {{end}}
    </div>
    <div>
        <!-- Toggle the links based on authentication status -->
        {{if .IsAuthenticated}}
            <form action='/user/logout' method='POST'>
                <button>Logout</button>
            </form>
        {{else}}
            <a href='/user/signup'>Signup</a>
            <a href='/user/login'>Login</a>
        {{end}}
    </div>
</nav>
{{end}}

تمام فایل‌ها را ذخیره کنید و اکنون برنامه را اجرا کنید. اگر در حال حاضر وارد نشده‌اید، صفحه اصلی برنامه شما باید به این صورت باشد:

10.06-01.png

در غیر این صورت — اگر وارد شده‌اید — صفحه اصلی شما باید به این صورت باشد:

10.06-02.png

احساس راحتی کنید و با این کار بازی کنید و سعی کنید وارد و خارج شوید تا مطمئن شوید که نوار ناوبری به درستی تغییر می‌کند.

محدود کردن دسترسی

همانطور که هست، ما لینک ناوبری ‘ایجاد قطعه’ را برای هر کاربری که وارد نشده است پنهان می‌کنیم. اما یک کاربر احراز هویت نشده هنوز می‌تواند با مراجعه به صفحه https://localhost:4000/snippet/create به طور مستقیم یک قطعه جدید ایجاد کند.

بیایید این را درست کنیم، به طوری که اگر یک کاربر احراز هویت نشده سعی کند به هر مسیری با مسیر URL /snippet/create مراجعه کند، به /user/login هدایت شود.

ساده‌ترین راه برای انجام این کار از طریق برخی از میان‌افزارها است. فایل cmd/web/middleware.go را باز کنید و یک تابع میان‌افزار جدید requireAuthentication() ایجاد کنید، با پیروی از همان الگویی که قبلاً در کتاب استفاده کردیم:

File: cmd/web/middleware.go
package main

...

func (app *application) requireAuthentication(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // If the user is not authenticated, redirect them to the login page and
        // return from the middleware chain so that no subsequent handlers in
        // the chain are executed.
        if !app.isAuthenticated(r) {
            http.Redirect(w, r, "/user/login", http.StatusSeeOther)
            return
        }

        // Otherwise set the "Cache-Control: no-store" header so that pages
        // require authentication are not stored in the users browser cache (or
        // other intermediary cache).
        w.Header().Add("Cache-Control", "no-store")

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

اکنون می‌توانیم این میان‌افزار را به فایل cmd/web/routes.go خود اضافه کنیم تا مسیرهای خاصی را محافظت کنیم.

در مورد ما، می‌خواهیم مسیرهای GET /snippet/create و POST /snippet/create را محافظت کنیم. و هیچ دلیلی برای خروج یک کاربر وجود ندارد اگر وارد نشده باشد، بنابراین منطقی است که از آن در مسیر POST /user/logout نیز استفاده کنیم.

برای کمک به این کار، بیایید مسیرهای برنامه خود را به دو ‘گروه’ تقسیم کنیم.

گروه اول شامل مسیرهای ‘غیرمحافظت شده’ ما خواهد بود و از زنجیره میان‌افزار dynamic موجود ما استفاده خواهد کرد. گروه دوم شامل مسیرهای ‘محافظت شده’ ما خواهد بود و از یک زنجیره میان‌افزار جدید protected استفاده خواهد کرد — که شامل زنجیره میان‌افزار dynamic به‌علاوه میان‌افزار جدید requireAuthentication() ما است.

به این صورت:

File: cmd/web/routes.go
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))

    // Unprotected application routes using the "dynamic" middleware chain.
    dynamic := alice.New(app.sessionManager.LoadAndSave)

    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 (authenticated-only) application routes, using a new "protected"
    // middleware chain which includes the requireAuthentication middleware.
    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)
}

فایل‌ها را ذخیره کنید، برنامه را مجدداً راه‌اندازی کنید و مطمئن شوید که خارج شده‌اید.

سپس سعی کنید به طور مستقیم به https://localhost:4000/snippet/create در مرورگر خود مراجعه کنید. باید متوجه شوید که بلافاصله به فرم ورود هدایت می‌شوید.

اگر دوست دارید، می‌توانید با curl نیز تأیید کنید که کاربران احراز هویت نشده برای مسیر POST /snippet/create نیز هدایت می‌شوند:

$ curl -ki -d "" https://localhost:4000/snippet/create
HTTP/2 303 
content-security-policy: default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com
location: /user/login
referrer-policy: origin-when-cross-origin
server: Go
vary: Cookie
x-content-type-options: nosniff
x-frame-options: deny
x-xss-protection: 0
content-length: 0
date: Wed, 18 Mar 2024 11:29:23 GMT

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

بدون استفاده از alice

اگر از بسته justinas/alice برای مدیریت میان‌افزار خود استفاده نمی‌کنید، مشکلی نیست — می‌توانید به صورت دستی هندلرهای خود را به این صورت بپیچید:

mux.Handle("POST /snippet/create", app.sessionManager.LoadAndSave(app.requireAuthentication(http.HandlerFunc(app.snippetCreate))))

واژه‌نامه اصطلاحات فنی

اصطلاح فارسی معادل انگلیسی توضیح
مجوز کاربر User Authorization کنترل دسترسی کاربران
کنترل دسترسی Access Control مدیریت سطوح دسترسی
محافظت از مسیرها Route Protection امن‌سازی مسیرهای برنامه
میان‌افزار احراز هویت Authentication Middleware کد واسط برای تأیید هویت
میان‌افزار Middleware کد واسط پردازش درخواست
بررسی مجوز Authorization Check بررسی مجوز دسترسی
نشست کاربر User Session اطلاعات نشست کاربری
هدایت مجدد Redirection انتقال به صفحه دیگر
مسیر محافظت شده Protected Route مسیر نیازمند مجوز
سطح دسترسی Access Level میزان مجوز دسترسی