Authorization trong Laravel


Authentication là xác thực thì Authorization là cấp phép. Xác thực chính là bạn rồi đó, nhưng bạn có quyền để thực hiện thao tác hay không lại là một câu chuyện khác. Nhằm dễ dàng phân quyền cho các yêu cầu phức tạp của hệ thống, Laravel đã xây dựng cho chúng ta các phương thức vô cùng đơn giản và dễ sử dụng. Cùng mình tìm hiểu ngay thôi nào!

Giới thiệu

Như mình giới thiệu ở trên, Authorization là việc điều kiển việc được truy cập của người dùng. Hay nói đơn giản hơn đó là phân quyền trong hệ thống. Thường thường thì chúng ta thấy, nếu làm một trang web quy mô nhỏ chỉ có 2 quyền đó là admin và user thì chúng ta chỉ cần viết middleware là xong. Hoặc đơn giản ta làm multiple authentication. Nhưng do nhu cầu hệ thống cần phân biệt quyền một cách phức tạp, chi tiết hơn nên Laravel sinh ra Authorization để hỗ trợ chúng ta trong việc phân quyền đó. Có 2 cách mà đã được hỗ trợ: GatesPolicies. Cùng nhau tìm hiểu từng cái thôi nào.

Gate

  • Gate là một Closures, nó định nghĩa alows/denies một hành động cụ thể nào đó của một user trên hệ thống.
  • Có thể hiểu Gate là một bộ lọc khác mà Laravel cung cấp.
  • Gate không liên quan tới model nào cả, Gate sẽ chỉ sử dụng thông tin được cung cấp bởi tham số, và thường sẽ được sử dụng ở tầng Controller, để kiểm tra user có được phép hay không.
  • Gate được định nghĩa trong method boot của AuthServiceProvider và sử dụng facade Gate. Do là chúng ta xác thực xem người dùng này có được làm hành động hay truy cập vào trang này trang nọ trong hệ thống của chúng ta hay không nên đối số đầu tiên trong hàm rằng buộc sẽ là một user instance, còn đối số thứ hai sẽ là một instance khác.

Define

Ví dụ như người dùng sẽ có quyền thay đổi comment của mình chẳng hạn:

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Auth;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();
       
        Gate::define('edit-comment', function ($user, $commment) {
            return $user->id == $comment->user_id;
        });
    }
}

Gate::define() sẽ định nghĩa một điều kiện giữa người dùng và thông tin comment, nếu trả về true thì người dùng được phép sửa comment và ngược lại thì không được sữa.

Cũng như Controllers, gates cũng có thể được định nghĩa bằng cách sử dụng một class callback:

use App\Policies\PostPolicy;
use Illuminate\Support\Facades\Gate;

/**
 * Register any authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Gate::define('update-comment', [CommentPolicy::class, 'update']);
}

alows - denies - forUser

Để kiểm tra người dùng có được phép thực hiện thao tác trên hệ thống hay không, ta tiến hành kiểm tra trong controller:

public function index()
{
    $comment = Comment::find(1);
    if (Gate::allows('edit-comment', $comment)) { 
        // $comment->user_id == Auth::user()->id
        echo "Ban co quyen chinh sua comment";
    } else {
        echo "Ban khong co quyen chinh sua comment";
    }

    if (Gate::denies('edit-comment', $comment)) { 
        // $comment->user_id != Auth::user()->id
        echo "Ban khong co quyen chinh sua comment";
    } else {
        echo "Ban co quyen chinh sua comment";
    }
}

Lưu ý: phải authentication trước khi authorization.

Bên trên chỉ là ví dụ minh họa cho việc kiểm tra bạn có quyền chỉnh sửa comment nào đó không. Các bạn không phải truyền argument $user vào, Laravel sẽ tự động truyền user đang đăng truy cập vào hệ thống vào trong Gate Closure.
Trái ngược với Gate::allows()Gate::denies(), kiểm tra nếu bạn không có quyền sẽ trả về true.

Nếu bạn muốn kiểm tra một $user bất kì nào đó có quyền thực hiện thao tác trên hệ thống hay không, ta sử dụng forUser:

if (Gate::forUser($user)->allows('edit-comment', $comment)) {
    echo "Người dùng này được phép chỉnh sửa comment";
}

if (Gate::forUser($user)->denies('edit-comment', $comment)) {
    echo "Người dùng này không được phép chỉnh sửa comment";
}

before - after

Để cấp quyền cho một người dùng nào đó trước khi các authorization được check. Thông thường đó là người quản trị website có tất cả các quyền trong hệ thống:

Gate::before(function ($user, $ability) {
    if ($user->isSuperAdmin()) {
        return true;
    }
});

Nếu bạn muốn một user không được phép thực hiện bất kỳ gì bạn chỉ cần trả về false trong phương thức before. Nếu giá trị được trả về là null, việc cấp quyền sẽ được tiếp tục diễn ra.

Và bạn có thể sử dụng Gate::after() để định nghĩa 1 callback để thực thi sau mỗi lần kiểm tra authorization. Tuy nhiên, các bạn sẽ không thay đổi được kết quả của sự kiểm tra phân quyền.

Gate::after(function ($user, $ability, $result, $arguments) {
    //
});

Policies

  • Cùng với gate, policy là một lựa chọn khác để phân quyền, cho phép hành động nào được phép thực hiện, hành động nào không.
  • Policies là các class quản lý logic trong phân quyền liên quan đến các Model hoặc tài nguyên nào đó (Thường gắn với một model cụ thể).
  • Giống với Gate, Policy cũng nhận tham số đầu tiên là một instance của user, và là user đã authen vào hệ thống.
  • Policy thường được sử dụng với các hành động CRUD của một model cụ thể.

Define

Để tạo một Policy đơn giản bằng command line:

php artisan make:policy CommentPolicy

Câu lệnh trên sẽ sinh ra một policy rỗng, nếu muốn sinh ra một CRUD policy thì chúng ta cần thêm tham số --model=Comment khi thực thi câu lệnh command line:

php artisan make:policy CommentPolicy --model=Comment
<?php

namespace App\Policies;

use App\Models\Comment;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class CommentPolicy
{
    use HandlesAuthorization;

    /**
     * Determine whether the user can view any models.
     *
     * @param  \App\Models\User  $user
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function viewAny(User $user)
    {
        //
    }

    /**
     * Determine whether the user can view the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Comment  $comment
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function view(User $user, Comment $comment)
    {
        //
    }

    /**
     * Determine whether the user can create models.
     *
     * @param  \App\Models\User  $user
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function create(User $user)
    {
        //
    }

    /**
     * Determine whether the user can update the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Comment  $comment
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function update(User $user, Comment $comment)
    {
        //
    }

    /**
     * Determine whether the user can delete the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Comment  $comment
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function delete(User $user, Comment $comment)
    {
        //
    }

    /**
     * Determine whether the user can restore the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Comment  $comment
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function restore(User $user, Comment $comment)
    {
        //
    }

    /**
     * Determine whether the user can permanently delete the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Comment  $comment
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function forceDelete(User $user, Comment $comment)
    {
        //
    }
}

Nhìn trông có vẻ giống cách tạo resource controller đúng không các bạn. Vì thế mà người ta ví Gate và Policy như là RouteController. Mình sẽ nói rõ phần này hơn nhé, khi chúng ta sử dụng

Gate::resource('comments', 'CommentPolicy');

thì với phương thức resource như vậy thì nó sẽ định nghĩa ra thủ công như sau:

Gate::define('comments.view', 'CommentPolicy@view');
Gate::define('comments.create', 'CommentPolicy@create');
Gate::define('comments.update', 'CommentPolicy@update');
Gate::define('comments.delete', 'CommentPolicy@delete');

Sau khi khởi tạo xong cần khai báo vào trong AuthServiceProvider.php. Đăng ký một policy sẽ chỉ dẫn cho Laravel biết policy nào sẽ được sử dụng để phân quyền hành động cho model nào:

<?php

namespace App\Providers;

use App\Comment;
use App\Policies\CommentPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        Comment::class => CommentPolicy::class,
    ];

    /**
     * Register any application authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();
    }
}

Writing Policy

Ở trên mình đã định nghĩa Gate, nó dùng để authorize những hành động riêng lẻ. Trong Policy cũng không khấc gì nhá, thay vì khai báo Gate trong function boot() chúng ta sẽ khai báo vào file Policy mà ta vừa khởi tạo:

<?php

namespace App\Policies;

use App\User;
use App\Comment;
use Illuminate\Auth\Access\HandlesAuthorization;

class PostPolicy
{
      use HandlesAuthorization;

    /**
     * Determine if the given post can be updated by the user.
     * @return  bool
     */
    public function update(User $user, Comment $comment)
    {
        return $user->id === $comment->user_id;
    }
}

Lưu ý: phương thức create trong policy chỉ nhận đối số đầu tiên là Model User chứ không nhận tham số thứ hai.

/**
 * Determine whether the user can create models.
 *
 * @param  \App\Models\User  $user
 * @return \Illuminate\Auth\Access\Response|bool
 */
public function create(User $user)
{
    //
}

Policy Filters

Thật ra phần này tương tự như Gate::before, chúng ta sẽ khai báo phương thức before trong policy. Phương thức này sẽ thực thi trước bất kỳ phương thức nào trong policy.

public function before($user, $ability)
{
    if ($user->isAdmin()) {
        return true;
    }
}

Nếu bạn muốn một user không được phép thực hiện bất kỳ gì bạn chỉ cần trả về false trong phương thức before. Nếu giá trị được trả về là null, việc cấp quyền sẽ được tiếp tục diễn ra trong phương thức policy.

Authorizing Actions Using Policies

Về bản chất, Policy cũng nạp vào Gate. Nó khác về cách dùng thôi. Vì vậy, 2 thằng này sẽ có cách sử dụng như nhau.
Chúng ta sử dụng nó thông qua 2 phương thức cancant được tạo sẵn trong Model User. Nhìn vào tên phương thức thì ta cũng đã hiểu chức năng nó làm gì rồi phải không.

if ($user->can('edit-comment', $comment)) {
    // ...
}

if ($user->cant('update', $comment)) {
    // ...
}

Các bạn chú ý, function create trong policy không yêu cầu tham số thứ 2 là instance của model nào. Trong trường hợp này, chúng ta có thể Pass trên class vào phương thức can. Tên lớp sẽ được sử dụng để xác định Policy nào sẽ sử dụng khi authorize

use App\Models\Comment;

if ($user->can('create', Comment::class)) {

}

Sử dụng trong Middleware

Laravel thêm một middleware có thể authorize action trước khi có request gửi đến ngay cả đến với routes hoặc controller. Mặc định, middleware Illuminate\Auth\Middleware\Authorize được gán key can trong class App\Http\Kernel

use App\Models\Comment;

Route::put('/comment/{comment}', function (Comment $comment) {
    // The current user may update the post...
})->middleware('can:update,comment');

Chú ý, vấn là phương thức create không require tham số thứ hai. Trong trường hợp này chúng ta sẽ pass tên class vào middleware. Tên class sẽ quyết định policy nào để sử dụng khi authorize hành động.

Route::post('/comment', function () {
    
})->middleware('can:create,App\Models\Comment');

Nên cẩn thận khi sử dụng authorize vào hệ thống, vì khi phân quyền sai thì sẽ gây ra sự khó chịu cho người dùng vì bị liên tục từ chối truy cập.

Sử dụng trong Controller

Ngoài phương thức can thì Laravel còn hỗ trỡ authorize đã được khai báo trong App\Http\Controlers\Controller mà controler đã được kế thừa.

<?php

namespace App\Http\Controllers;

use App\Comment;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class CommentController extends Controller
{
    
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function update(Request $request, Comment $comment)
    {
        $this->authorize('update', $comment);
        // Người dùng có thể update commmet của mình
    }
}

cũng là phương thức create của policy, ra cũng truyền class Model vào

<?php

namespace App\Http\Controllers;

use App\Comment;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class CommentController extends Controller
{
    
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function create(Request $request)
    {
        $this->authorize('create', Comment::class);
        // Người dùng có thể create commnet của mình
    }
}

Sử dụng trong Blade Template

Cái này được sử dụng rất nhiều, chặn trước từ người dùng, hạn chế trả về 403. Phần lớn phân quyền chúng ta sử dụng cho việc quản lý, nên khi áp dụng nhiều vào blade cũng không sợ bị ảnh hưởng tốc độ load. Vì admin load chậm một xíu cũng chả ảnh hưởng gì phải không nào.
Chúng ta sử dụng @can@cannot để kiểm tra:

@can('update', $comment)
    // hiển thị button edit
@endcan

@cannot('update', $comment)
    // Khong hiển thị button edit
@endcannot

Tiếp tục là thằng create của policy:

@can('create', App\Models\Comment::class)
    //
@endcan

@cannot('create', App\Models\Comment::class)
    //
@endcannot

Kết luận

Phía trên mình giới thiệu một số chứng năng phổ biến mà mình cũng như những hệ thống khác áp dụng. Không cần phải tìm hiểu hết 100% các chức năng của cái này đâu, căn bản nhưng nắm vững thì vẫn tốt hơn mà.

Khi làm các hệ thống nhỏ thì các bạn thấy Authorization nó cồng kềnh, không cần thiết. Chúng ta có thể tự kiểm tra trong từng controller. Nhưng khi làm những hệ thống hơi bự và có độ phức tập của việc phân quyền nhất định các bạn sẽ thấy nó hữu ích. Chúng ta dễ dàng quản lý và sử dụng cho hệ thống.

Nếu có bất kì thắc mắc gì hãy để lại comment ở phía dưới nhé.
Nguồn tham khảo:

Rất mong được sự ủng hộ của mọi người để mình có động lực ra những bài viết tiếp theo.
{\__/}
( ~.~ )
/ > ♥️ I LOVE YOU 3000

JUST DO IT!


Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Nguyễn Quang Đạt !
Comments
  TOC