Phân quyền người dùng với Laravel Authorization

Bất kể hệ thống website nào cũng có người dùng và đi kèm với nó là việc xác thực (authentication) và phân quyền (authorization) với từng người dùng. Xác thực trong Laravel là khá đơn giản với Laravel Authentication, chúng ta cũng có thể tận dụng các hệ thống khác như mạng xã hội để xác thực, ví dụ xác thực người dùng bằng Facebook, Google, Twitter…  Bên cạnh đó, chúng ta cũng rất cần phân quyền cho người dùng để đảm bảo các vấn đề về bảo mật và tuân thủ các chính sách từng hệ thống. Ví dụ như có những người dùng được phép chỉnh sửa hoặc xóa một tài nguyên nhưng cũng có những người dùng chỉ được phép đọc thông tin. Từ phiên bản Laravel 5.1 trở về trước, công việc này phải thực hiện thông qua các gói ACL (Access Control List) như Entrust, Sentinel hay Laravel-ACL thì hiện nay Laravel đã có hỗ trợ trực tiếp trong core của framework.

Với việc sử dụng các gói ACL ở ngoài, các quyền cho người dùng chỉ là các cờ, nếu muốn các nghiệp vụ phức tạp trong phân quyền, bạn sẽ rất khó khăn và phải đưa vào Controller. Sử dụng Laravel Gate tránh được một số các nhược điểm của việc sử dụng các gói ACL ngoài như sau:

  • Sử dụng Laravel Gate cho bạn sự tự do, bạn có thể sử dụng cho các trường hợp cực phức tạp tùy ý do nó không bắt buộc phải implement trong các model.
  • Bạn có hoàn toàn tự chủ về các chính sách phân quyền, với Laravel Gate các xử lý truy cập có thể tách biệt với các xử lý về nghiệp vụ, loại bỏ sự phụ thuộc và code trong các controller dễ đọc hơn.

Gate và Policy giống như Route với Controller, Gate cũng cấp một giải pháp dựa trên Closure để phân quyền trong khi các policy giống với controller nhóm các logic nghiệp vụ liên quan đến Model hoặc các tài nguyên. Chúng ta cùng tìm hiểu về Gate trước khi tìm hiểu về Policy.
Phần lớn các ứng dụng sẽ sử dụng cả Gate và Policy, Gate được áp dụng cho các hành động không liên quan đến Model hoặc các tài nguyên như việc truy nhập vào trang quản trị dashboard. Ngược lại, policy được sử dụng khi bạn muốn cho phép một hành động truy nhập vào một model hoặc nguồn tài nguyên.

1. Laravel Gate

Gate là các Closure được xác định nếu một người dùng được xác thực để thực hiện một hành động, nó được định nghĩa trong App\Providers\AuthServiceProvider sử dụng facade Gate. Gate luôn nhận một thực thể user là tham số thứ nhất và có thể có các tham số tùy chọn khác như Eloquent Model:

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

    Gate::define('update-post', function ($user, $post) {
        return $user->id == $post->user_id;
    });
}

Gate cũng có thể định nghĩa sử dụng dạng callback string giống như Controller:

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

    Gate::define('update-post', 'PostPolicy@update');
}

Resource Gates
Bạn có thể định nghĩa nhiều Gate sử dụng phương thức resource

Gate::resource('posts', 'PostPolicy');

Với phương thức resource nó tương tự như định nghĩa thủ công các phương thức sau:

Gate::define('posts.view', 'PostPolicy@view');
Gate::define('posts.create', 'PostPolicy@create');
Gate::define('posts.update', 'PostPolicy@update');
Gate::define('posts.delete', 'PostPolicy@delete');

Mặc định view, create, update và delete được định nghĩa. Bạn có thể override các khả năng này bằng cách truyền vào một mảng như là tham số thứ 3 cho phương thức resource. Key của mảng này định nghĩa tên khả năng trong khi giá trị định nghĩa tên phương thức.

Gate::resource('posts', 'PostPolicy', [
    'photo' => 'updatePhoto',
    'image' => 'updateImage',
]);

Phân quyền
Để phân quyền thực hiện một hành động, bạn có thể sử dụng các phương thức allows và denies, chú ý rằng bạn không cần truyền người dùng đã được xác thực vào các phương thức này. Laravel sẽ tự động xử lý việc đó trong các gate Closure:

if (Gate::allows('update-post', $post)) {
    // The current user can update the post...
}

if (Gate::denies('update-post', $post)) {
    // The current user can't update the post...
}

Nếu bạn muốn xác định một người dùng nào đó có được phân quyền để thực hiện một hành động hay không, bạn có thể sử dụng phương thức forUser trên facade Gate:

if (Gate::forUser($user)->allows('update-post', $post)) {
    // The user can update the post...
}

if (Gate::forUser($user)->denies('update-post', $post)) {
    // The user can't update the post...
}

2. Policy

2.1 Tạo policy

Policy là các class quản lý logic trong phân quyền liên quan đến một Model hoặc tài nguyên nào đó. Ví dụ, nếu ứng dụng của bạn là một blog, bạn có thể có một model Post và một policy là PostPolicy để phân quyền các hành động người dùng như tạo hay cập nhật các bài viết.
Bạn có thể tạo ra một policy bằng cách sử dụng câu lệnh Artisan make:policy, các policy được tạo ra sẽ được đặt trong thư mục app\Policies. Nếu thư mục này không tồn tại trong project, Laravel sẽ tự động tạo nó cho bạn:

php artisan make:policy PostPolicy

Câu lệnh make:policy sẽ sinh ra một class policy rỗng, nếu bạn muốn sinh ra một CRUD policy bạn cần thêm tham số –model khi thực thi câu lệnh artisan:

php artisan make:policy PostPolicy --model=Post

2.2 Đăng ký Policy

Một policy muốn sử dụng cần được đăng ký, AuthServiceProvider được đưa vào trong project Laravel chứa một thuộc tính policies để map Eloquent model với các policy tương ứng. Đăng ký một policy sẽ chỉ dẫn cho Laravel 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\Post;
use App\Policies\PostPolicy;
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 = [
        Post::class => PostPolicy::class,
    ];

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

        //
    }
}

2.3 Write Policy

Phương thức Policy
Khi Policy được đăng ký, bạn có thể thêm các phương thức cho mỗi hành động cần cấp quyền. Ví dụ, định nghĩa phương thức update trên PostPolicy để xác định một user có thể cập nhật một thực thể Post. Phương thức update sẽ nhận được một User và Post như là tham số và nó trả về true hoặc false để nhận diện xem người dùng này có được phân quyền để cập nhật Post không? Trong ví dụ dưới đây, chỉ có người dùng đã viết bài mới có quyền cập nhật bài viết

<?php

namespace App\Policies;

use App\User;
use App\Post;

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

Bạn có thể tiếp tục định nghĩa các phương thức khác trên policy này nếu cần phân quyền cho các hành động. Ví dụ, bạn có thể định nghĩa phương thức view hoặc delete để phân quyền các hành động Post khác và nhớ rằng tên các phương thức trong policy là hoàn toàn bạn có thể đặt thoải mái nếu thích.

2.4 Các phương thức không có Model

Có những phương thức policy chỉ nhận người dùng được xác thực hiện tại và không cần một thực thể của một Model. Tình huống này dùng khi phân quyền một hành động create. Ví dụ, nếu bạn tạo một blog, bạn có thể muốn kiểm tra nếu một người dùng có được phân quyền để tạo một post bất kỳ không.
Khi định nghĩa phương thức trong policy nó sẽ không nhận một thực thể của Model, như phương thức create, nó sẽ không nhận một thực thể của model. Thay vào đó, bạn nên định nghĩa phương thức chỉ với người dùng đã được xác thực.

/**
 * Determine if the given user can create posts.
 *
 * @param  \App\User  $user
 * @return bool
 */
public function create(User $user)
{
    //
}

2.5 Policy Filter

Với người dùng hiện tại, bạn muốn cấp quyền thực hiện các hành động trong một chính sách, để thực hiện bạn định nghĩa phương thức before trong policy. Phương thức before sẽ thực thi trước bất kỳ phương thức nào trong policy, nó cho bạn cơ hội để cho phép thực hiện hành động trước khi phương thức của policy mong muốn được gọi. Tính năng này rất thông dụng để cho phép các administrator có thể thực hiện bất kỳ hành động nào:

public function before($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 null được trả về, việc cấp quyền sẽ được tiếp tục trong phương thức policy.

3. Cho phép thực hiện hành động sử dụng Policy

Thông qua User Model
Model User được tạo sẵn trong project Laravel chứa hai phương thức có sẵn là can và cant. Phương thức can nhận hành động bạn muốn cấp phép và model liên quan. Ví dụ, để xác định một người dùng được phép cập nhật model Post không?

if ($user->can('update', $post)) {
    //
}

4. Thực hành Laravel Authorization với một ví dụ thực tế

Chúng ta cùng xem xét một ví dụ về phân quyền trong một tờ báo điện tử bao gồm hai chức danh là phóng viên và biên tập viên.

  • Phóng viên có thể viết một bài báo mới
  • Phóng viên có thể cập nhật nội dung bài báo của họ.
  • Biên tập viên có thể cập nhật nội dung bài báo.
  • Biên tập viên có thể phát hành bài báo.

Như vậy phóng viên không thể tự phát hành bài báo và biên tập viên không thể tạo ra bài báo mới mà chỉ có quyền cập nhật và xuất bản. Chúng ta sẽ từng bước thiết kế và phát triển ứng dụng web này, nó sẽ giúp bạn hiểu cạn kẽ về Laravel Authorization.

4.1 Cài đặt môi trường Laravel

Cài đặt môi trường Laravel rất đơn giản, bạn tham khảo bài Cài đặt Laravel dễ dàng với Laragon (trong bài này sẽ lấy tên project là allaravel nhé). Tiếp theo, chúng ta thực hiện cài đặt xác thực với Laravel Authentication. Đây là những phần rất cơ bản của Laravel, bạn nên làm quen vì hầu hết các dự án mới sẽ bắt đầu bằng bước này.

Kiểm tra xem môi trường hoạt động tốt chưa, mở đường dẫn http://allaravel.dev.

4.2 Tạo database và dữ liệu kiểm thử với Laravel Migrate và Laravel Seeding

Việc đầu tiên của dự án báo điện tử là thiết kế database, chúng ta sẽ có 4 bảng như sau:

  • Posts: chứa các bài báo
  • Users: chứa thông tin người dùng hệ thống, được tạo khi sử dụng Laravel Authentication.
  • Roles: chứa các chức danh và quyền hạn của từng chức danh
  • Role_users: gán từng người dùng với chức danh.

Thiết kế cơ sở dữ liệu

Bạn nên tham khảo về Laravel Migrate và Laravel Seeding nếu chưa làm quen các khái niệm này.

4.2.1 Tạo file migrate cho cơ sở dữ liệu

Đầu tiên, chúng ta sử dụng câu lệnh artisan để tạo ra model Post kèm theo với controller PostController và file migrate bằng cách sử dụng các tùy chọn -m và -c.

php artisan make:model Post -m -c

Với câu lệnh này chúng ta có:

  • Model Post trong file app\Post.php.
  • Post Controller trong file app\Http\Controllers\PostController.php.
  • File migrate nằm trong databases\migrations\yyyy_mm_dd_xxxxxx_create_post_table.php.

Xử lý file migrate để tạo bảng post trong cơ sở dữ liệu, thiết lập các trường của bảng post như sau:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->string('slug')->unique();
            $table->text('body');
            $table->boolean('published')->default(false);
            $table->unsignedInteger('user_id');
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

Tiếp đến chúng ta tạo model Role kèm theo controller và file migrate:

php artisan make:model Role -m -c

Tạo các trường cho bảng roles với file migrate databases\migrations\yyyy_mm_dd_xxxxxx_create_roles_table.php:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateRolesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('slug')->unique();
            $table->jsonb('permissions')->default('{}');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('roles');
    }
}

Chú ý: trường permission chúng ta sẽ chứa một dữ liệu json thay vì tách ra thành một bảng riêng.

Tiếp theo là bảng role_users chỉ cần tạo bảng do đó sử dụng câu lệnh artisan make:migrate

php artisan make:migration create_role_users_table

và định nghĩa các trường của bảng role_users trong file migrate databases\migrations\yyyy_mm_dd_xxxxxx_create_role_users_table

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateRoleUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('role_users', function (Blueprint $table) {
            $table->unsignedInteger('user_id');
            $table->unsignedInteger('role_id');
            $table->timestamps();

            $table->unique(['user_id','role_id']);
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
            $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('role_users');
    }
}

4.2.2 Thiết kế model map từ database sang class

Trong phần 4.1 chúng ta đã cài đặt phần xác thực cho Laravel, nó tạo ra bảng user, model User tuy nhiên model này chưa có phần phân quyền. Chúng ta sẽ thêm hai phương thức cho model User:

  • hasAccess(): kiểm tra xem người dùng có quyền thực hiện một hành động nào đó không.
  • inRole(): kiểm tra xem một người dùng có thuộc về một chức danh nào đó không.
<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    protected $fillable = [
        'name', 'email', 'password',
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

    public function roles()
    {
        return $this->belongsToMany(Role::class, 'role_users');
    }

    /**
     * Checks if User has access to $permissions.
     */
    public function hasAccess(array $permissions) : bool
    {
        // check if the permission is available in any role
        foreach ($this->roles as $role) {
            if($role->hasAccess($permissions)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks if the user belongs to role.
     */
    public function inRole(string $roleSlug)
    {
        return $this->roles()->where('slug', $roleSlug)->count() == 1;
    }
}

Model User có quan hệ nhiều – nhiều với model Role thông qua bảng role_users (Xem bài xử lý quan hệ trong database với Laravel ORM Eloquent). Tiếp theo, chúng ta thay đổi model Role

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    protected $fillable = [
        'name', 'slug', 'permissions',
    ];
    protected $casts = [
        'permissions' => 'array',
    ];

    public function users()
    {
        return $this->belongsToMany(User::class, 'role_users');
    }

    public function hasAccess(array $permissions) : bool
    {
        foreach ($permissions as $permission) {
            if ($this->hasPermission($permission))
                return true;
        }
        return false;
    }

    private function hasPermission(string $permission) : bool
    {
        return $this->permissions[$permission] ?? false;
    }
}

và cuối cùng là model Post

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = [
        'title', 'slug', 'body', 'user_id'
    ];

    public function owner()
    {
        return $this->belongsTo(User::class);
    }

    public function scopePublished($query)
    {
        return $query->where('published', true);
    }

    public function scopeUnpublished($query)
    {
        return $query->where('published', false);
    }
}

Trong model Post sử dụng hai local scope là published và unpublished để tiện cho các truy vấn sau này.

4.2.3 Tạo dữ liệu test

Chúng ta đã tạo xong các file migrate, chúng ta thêm dữ liệu kiểm thử vào bằng Laravel Seeding. Tạo các file seeder cho Role, User và Post như sau:

php artisan make:seeder RolesSeeder
php artisan make:seeder UsersSeeder
php artisan make:seeder PostsSeeder

Chúng ta sẽ tạo dữ liệu kiểm thử trong file vừa tạo ra databases\seeds\RolesSeeder.php như sau:

<?php

use Illuminate\Database\Seeder;
use App\Role;

class RolesSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $author = Role::create([
            'name' => 'Phóng viên', 
            'slug' => 'author',
            'permissions' => [
                'post.create' => true,
            ]
        ]);
        $editor = Role::create([
            'name' => 'Biên tập viên', 
            'slug' => 'editor',
            'permissions' => [
                'post.update' => true,
                'post.publish' => true,
            ]
        ]);
    }
}

Role author sẽ sử dụng cho phóng viên còn role editor sẽ sử dụng cho biên tập viên. Tiếp đến là databases\seeds\UsersSeeder.php:

<?php

use Illuminate\Database\Seeder;
use App\User;
use App\Role;

class UsersSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $author = Role::where('slug', 'author')->first();
        $editor = Role::where('slug', 'editor')->first();
        
        $user1 = User::create([
            'name' => 'Phóng viên 1', 
            'email' => 'pv1@allaravel.dev',
            'password' => bcrypt('123456')
        ]);
        $user1->roles()->attach($author);
        
        $user2 = User::create([
            'name' => 'Phóng viên 2', 
            'email' => 'pv2@allaravel.dev',
            'password' => bcrypt('123456')
        ]);
        $user2->roles()->attach($author);
        
        $user3 = User::create([
            'name' => 'Biên tập viên 1', 
            'email' => 'btv1@allaravel.dev',
            'password' => bcrypt('123456')
        ]);
        $user2->roles()->attach($editor);
    }
}

và databases\seeds\PostsSeeder.php:

<?php

use Illuminate\Database\Seeder;
use App\User;
use App\Post;

class PostsSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $author1 = User::where('email', 'pv1@allaravel.dev')->first();
        $author2 = User::where('email', 'pv2@allaravel.dev')->first();
        $faker = Faker\Factory::create();
        for ($i=0; $i < 10; $i++) { 
        	$title = $faker->sentence($nbWords = 6, $variableNbWords = true);
        	$post = Post::create([
	            'title' => $title, 
	            'body' => $faker->text($maxNbChars = 1000),
	            'slug' => str_slug($title),
	            'published' => rand(0,1),
	            'user_id' => $author1->id
	        ]);
	        $title = $faker->sentence($nbWords = 6, $variableNbWords = true);
        	$post = Post::create([
	            'title' => $title, 
	            'body' => $faker->text($maxNbChars = 1000),
	            'slug' => str_slug($title),
	            'published' => rand(0,1),
	            'user_id' => $author2->id
	        ]);
        }
    }
}

Chú ý, gọi đến các Seeder này trong databases\seeds\DatabaseSeeder.php:

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $this->call(\RolesSeeder::class);
        $this->call(\UsersSeeder::class);
        $this->call(\PostsSeeder::class);
    }
}

Ok, vậy là phần chuẩn bị khung cho cơ sở dữ liệu và đưa vào dữ liệu kiểm thử đã xong, để thực hiện các công việc này trên database sử dụng lệnh artisan migrate với tùy chọn –seed

php artisan migrate --seed

4.3 Thiết lập chính sách phân quyền

Câu lệnh make:policy giúp tạo ra một class policy để chứa các logic về phân quyền. Trong ví dụ hiện tại, chúng ta có model Post tương ứng với class PostPolicy để phân quyền các hành động của người dùng như tạo mới bài viết, cập nhật bài viết.

php artisan make:policy PostPolicy --model=Post
Policy created successfully.

Thiết lập các chính sách truy cập trong file app\Policies\PostPolicy.php

<?php

namespace App\Policies;

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

class PostPolicy
{
    use HandlesAuthorization;

    public function view(User $user, Post $post)
    {
        //
    }

    public function create(User $user)
    {
        return $user->hasAccess(['post.create']);
    }

    public function update(User $user, Post $post)
    {
        return $user->hasAccess(['post.update']) or $user->id == $post->user_id;
    }

    public function delete(User $user, Post $post)
    {
        //
    }

    public function publish(User $user)
    {
        return $user->hasAccess(['post.publish']);
    }

    public function draft(User $user)
    {
        return $user->inRole('editor');
    }
}

Đăng ký chính sách với hệ thống trong app\Providers\AuthServiceProvider.php:

<?php

namespace App\Providers;

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

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::resource('post', PostPolicy::class);
        Gate::define('post.publish', PostPolicy::class . '@publish');
        Gate::define('post.draft', PostPolicy::class . '@draft');
    }
}

Định nghĩa các route liên quan đến post và có kiểm tra quyền truy xuất trong routes\web.php

Route::get('/', 'PostController@index');
Route::get('/posts', 'PostController@index')->name('list_posts');
Route::group(['prefix' => 'posts'], function () {
    Route::get('/drafts', 'PostController@drafts')
        ->name('list_drafts')
        ->middleware('auth');
    Route::get('/show/{id}', 'PostController@show')
        ->name('show_post');
    Route::get('/create', 'PostController@create')
        ->name('create_post')
        ->middleware('can:post.create');
    Route::post('/create', 'PostController@store')
        ->name('store_post')
        ->middleware('can:post.create');
    Route::get('/edit/{post}', 'PostController@edit')
        ->name('edit_post')
        ->middleware('can:post.update,post');
    Route::post('/edit/{post}', 'PostController@update')
        ->name('update_post')
        ->middleware('can:post.update,post');
    Route::get('/publish/{post}', 'PostController@publish')
        ->name('publish_post')
        ->middleware('can:post.publish');
});

4.4 Các phần phát triển ứng dụng khác

Trong phần này, chúng ta sẽ xử lý các công việc còn lại là tạo ra các view và chuẩn bị dữ liệu cho các view này thông qua controller. Trong phần 4.2 khi tạo ra model Post chúng ta cũng đã tạo ra PostController, thực hiện xử lý logic cho từng route trước khi định tuyến đến view.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Post;
use App\Http\Requests\StorePost as StorePostRequest;
use App\Http\Requests\UpdatePost as UpdatePostRequest;
use Auth;
use Gate;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::published()->paginate();
        return view('posts.index', compact('posts'));
    }

    public function create()
    {
        return view('posts.create');
    }

    public function store(StorePostRequest $request)
    {
        $data = $request->only('title', 'body');
        $data['slug'] = str_slug($data['title']);
        $data['user_id'] = Auth::user()->id;
        $post = Post::create($data);
        return redirect()->route('edit_post', ['id' => $post->id]);
    }

    public function edit(Post $post)
    {
        return view('posts.edit', compact('post'));
    }

    public function update(Post $post, UpdatePostRequest $request)
    {
        $data = $request->only('title', 'body');
        $data['slug'] = str_slug($data['title']);
        $post->fill($data)->save();
        return back();
    }

    public function show($id)
    {
        $post = Post::published()->findOrFail($id);
        return view('posts.show', compact('post'));
    }

    public function publish(Post $post)
    {
        $post->published = true;
        $post->save();
        return back();
    }

    public function drafts()
    {
        $postsQuery = Post::unpublished();
        if(Gate::denies('post.draft')) {
            $postsQuery = $postsQuery->where('user_id', Auth::user()->id);
        }
        $posts = $postsQuery->paginate();
        return view('posts.drafts', compact('posts'));
    }
}

Tiếp theo, thực hiện tạo master layout và các view phục vụ cho các route liên quan. Tạo file app\resources\views\layouts\app.blade.php:

<!DOCTYPE html>
<html lang="vi">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="Ví dụ phân quyền trong báo điện tử sử dụng Laravel Authorization">
        <meta name="author" content="FB [allaravel.com]">
        <title>Ví dụ Laravel Authorization - Allaravel.com</title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
        <link href="https://getbootstrap.com/docs/3.3/examples/jumbotron/jumbotron.css" rel="stylesheet">
    </head>
    <body>
        <nav class="navbar navbar-inverse navbar-fixed-top">
            <div class="container">
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                        <span class="sr-only">Toggle navigation</span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    <a class="navbar-brand" href="">Allaravel</a>
                </div>
                <div class="collapse navbar-collapse" id="app-navbar-collapse">
                    <ul class="nav navbar-nav navbar-right">
                        @if (Auth::guest())
                            <li><a href="{{ route('login') }}">Đăng nhập</a></li>
                            <li><a href="{{ route('register') }}">Đăng ký</a></li>
                        @else
                            <li class="dropdown">
                                <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
                                    {{ Auth::user()->name }} <span class="caret"></span>
                                </a>

                                <ul class="dropdown-menu" role="menu">
                                    <li>
                                        <a href="{{ route('logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();"> Đăng xuất </a>
                                        <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                                            {{ csrf_field() }}
                                        </form>
                                    </li>
                                    <li>
                                        <a href="{{ route('list_drafts') }}">Bài chưa đăng</a>
                                    </li>
                                </ul>
                            </li>
                        @endif
                    </ul>
                </div>
            </div>
        </nav>
        <div class="container" style="margin-top: 50px;">
            @yield('content')
            <hr>
            <footer>
                <p>&copy; 2017 <a href="https://allaravel.com">Allaravel.com</a>.</p>
            </footer>
        </div>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
    </body>
</html>

Các view cho các hành động với user như đăng nhập, đăng ký.

app\resources\views\auth\login.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Đăng nhập</div>
                <div class="panel-body">
                    <form class="form-horizontal" method="POST" action="{{ route('login') }}">
                        {{ csrf_field() }}

                        <div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}">
                            <label for="email" class="col-md-4 control-label">Địa chỉ email</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control" name="email" value="{{ old('email') }}" required autofocus>

                                @if ($errors->has('email'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('email') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group{{ $errors->has('password') ? ' has-error' : '' }}">
                            <label for="password" class="col-md-4 control-label">Mật khẩu</label>

                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control" name="password" required>

                                @if ($errors->has('password'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('password') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group">
                            <div class="col-md-6 col-md-offset-4">
                                <div class="checkbox">
                                    <label>
                                        <input type="checkbox" name="remember" {{ old('remember') ? 'checked' : '' }}> Ghi nhớ
                                    </label>
                                </div>
                            </div>
                        </div>

                        <div class="form-group">
                            <div class="col-md-8 col-md-offset-4">
                                <button type="submit" class="btn btn-primary">
                                    Đăng nhập
                                </button>

                                <a class="btn btn-link" href="{{ route('password.request') }}">
                                    Quên mật khẩu?
                                </a>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

app\resources\views\auth\register.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Đăng ký tài khoản</div>
                <div class="panel-body">
                    <form class="form-horizontal" method="POST" action="{{ route('register') }}">
                        {{ csrf_field() }}

                        <div class="form-group{{ $errors->has('name') ? ' has-error' : '' }}">
                            <label for="name" class="col-md-4 control-label">Họ và tên</label>

                            <div class="col-md-6">
                                <input id="name" type="text" class="form-control" name="name" value="{{ old('name') }}" required autofocus>

                                @if ($errors->has('name'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('name') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}">
                            <label for="email" class="col-md-4 control-label">Địa chỉ Email</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control" name="email" value="{{ old('email') }}" required>

                                @if ($errors->has('email'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('email') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group{{ $errors->has('password') ? ' has-error' : '' }}">
                            <label for="password" class="col-md-4 control-label">Mật khẩu</label>

                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control" name="password" required>

                                @if ($errors->has('password'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('password') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group">
                            <label for="password-confirm" class="col-md-4 control-label">Nhập lại mật khẩu</label>

                            <div class="col-md-6">
                                <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required>
                            </div>
                        </div>

                        <div class="form-group{{ $errors->has('role') ? ' has-error' : '' }}">
                            <label for="role" class="col-md-4 control-label">Chức danh</label>

                            <div class="col-md-6">
                                <select id="role" class="form-control" name="role" required>
                                @foreach($roles as $id => $role)
                                    <option value="{{$id}}">{{$role}}</option>
                                @endforeach
                                </select>

                                @if ($errors->has('role'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('role') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group">
                            <div class="col-md-6 col-md-offset-4">
                                <button type="submit" class="btn btn-primary">
                                    Đăng ký
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

Các view cho việc tạo, cập nhật bài viết và các hành động khác như danh sách bài viết chưa đăng, hiển thị chi tiết

app\resources\views\posts\index.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        @foreach($posts as $post)
        <div class="col-md-4">
            <h2>{{ $post->title }}</h2>
            <p>{{ str_limit($post->body, 100) }}</p>
            <p><a class="btn btn-default" href="{{ route('show_post', ['id' => $post->id]) }}" role="button">Chi tiết &raquo;</a></p>
        </div>
        @endforeach
    </div>
</div>
@endsection

app\resources\views\posts\create.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-12">
            <div class="panel panel-default">
                <div class="panel-heading">Bài viết mới</div>
                <div class="panel-body">
                    <form class="form-horizontal" role="form" method="POST" action="{{ route('store_post') }}">
                        {{ csrf_field() }}
                        <div class="form-group{{ $errors->has('title') ? ' has-error' : '' }}">
                            <label for="title" class="col-md-3 control-label">Tiêu đề</label>
                            <div class="col-md-9">
                                <input id="title" type="text" class="form-control" name="title" value="{{ old('title') }}" required autofocus>
                                @if ($errors->has('title'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('title') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>
                        <div class="form-group{{ $errors->has('body') ? ' has-error' : '' }}">
                            <label for="body" class="col-md-3 control-label">Nội dung</label>
                            <div class="col-md-9">
                                <textarea name="body" id="body" cols="30" rows="8" class="form-control" required>{{ old('body') }}</textarea>
                                @if ($errors->has('body'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('body') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-md-9 col-md-offset-3">
                                <button type="submit" class="btn btn-success">
                                    Tạo bài viết mới
                                </button>
                                <a href="{{ route('list_posts') }}" class="btn btn-success">
                                    Hủy bỏ
                                </a>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

app\resources\views\posts\edit.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-12">
            <div class="panel panel-default">
                <div class="panel-heading">Cập nhật bài viết</div>
                <div class="panel-body">
                    <form class="form-horizontal" role="form" method="POST" action="{{ route('update_post', ['post' => $post->id]) }}">
                        {{ csrf_field() }}
                        <div class="form-group{{ $errors->has('title') ? ' has-error' : '' }}">
                            <label for="title" class="col-md-3 control-label">Tiêu đề</label>
                            <div class="col-md-9">
                                <input id="title" type="text" class="form-control" name="title" value="{{ old('title', $post->title) }}" required autofocus>
                                @if ($errors->has('title'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('title') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>
                        <div class="form-group{{ $errors->has('body') ? ' has-error' : '' }}">
                            <label for="body" class="col-md-3 control-label">Nội dung</label>
                            <div class="col-md-9">
                                <textarea name="body" id="body" cols="30" rows="8" class="form-control" required>{{ old('body', $post->body) }}</textarea>
                                @if ($errors->has('body'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('body') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-md-9 col-md-offset-3">
                                <button type="submit" class="btn btn-success">
                                    Cập nhật
                                </button>
                                @can('post.publish')
                                <a href="{{ route('publish_post', ['post' => $post->id]) }}" class="btn btn-success">
                                    Xuất bản
                                </a>
                                @endcan
                                <a href="{{ route('list_posts') }}" class="btn btn-default">
                                    Hủy bỏ
                                </a>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

app\resources\views\posts\drafts.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        @foreach($posts as $post)
        <div class="col-md-4">
            <h2>{{ $post->title }}</h2>
            <p>{{ str_limit($post->body, 100) }}</p>
            <p>
                <a class="btn btn-success" href="{{ route('edit_post', ['id' => $post->id]) }}" role="button">Chỉnh sửa</a>
                @can('post.publish')
                    <a class="btn btn-success" href="{{ route('publish_post', ['id' => $post->id]) }}" role="button">Xuất bản</a>
                @endcan
            </p>
        </div>
        @endforeach
    </div>
</div>
@endsection

app\resources\views\posts\show.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-12">
            <h2>{{ $post->title }}</h2>
            
            <p>{{ $post->body }}</p>
        </div>
    </div>
</div>
@endsection

app\resources\views\errors\403.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-12">
            <h2>Bạn không có quyền truy nhập đường dẫn này.</h2>
        </div>
    </div>
</div>
@endsection

4.5 Chạy thử ứng dụng

 

Khi vào màn hình trang chủ http://allaravel.dev, chúng ta sẽ thấy một số các bài viết đã được xuất bản

Màn hình trang chủ

Khi đăng nhập vào bằng user pv1@allaravel.dev và pv2@allaravel.dev chúng ta thấy các tài khoản này chỉ được tạo mới và cập nhật bài viết.

Chỉnh sửa và tạo bài viết mới

Phóng viên 1 có thể chỉnh sửa bài viết có id 32 và sửa đường dẫn thành http://allaravel.dev/posts/publish/32 để xuất bản bài viết nhưng do Phóng viên 1 không có quyền xuất bản do đó sẽ gặp thông báo lỗi “Bạn không có quyền truy nhập đường dẫn này”.

403 không có quyền truy cập

Đăng nhập vào user Biên tập viên 1, chúng ta thấy user này không có quyền tạo bài viết mới.

Chỉnh sửa và xuất bản

 

 

13 thoughts on “Phân quyền người dùng với Laravel Authorization

  1. Cám ơn bài viết của bạn. MÌnh thấy đây là một bài viết ý nghĩa. Nhưng có một số chỗ mình thắc mắc, mình có thể hỏi rõ bạn hơn được không?

  2. Trong bảng roles cái trường permissions: tại sao định dạng là jsonb, mình cũng chưa hiểu ý nghĩ của trường này. Bạn có thể giúp mình rõ được không?

    1. Bảng roles chứa các chức danh (role) trong hệ thống, ví dụ: quản trị viên, phóng viên, bán hàng…, với mỗi một chức danh sẽ có một danh sách các quyền (permission), ví dụ phóng viên có quyền xem danh sách các bài viết, tạo bài viết mới, chỉnh sửa bài viết… Như vậy trường permissions trong bảng roles chứa danh sách các quyền, thực tế thường tách ra thành một bảng permissions riêng nhưng ở đây mình gộp vào sử dụng dữ liệu dạng json. Bạn có thể tham khảo thêm bài viết https://allaravel.com/laravel-tutorials/laravel-eloquent-orm-phan-3-xu-ly-du-lieu-dau-ra/ để hiểu hơn về các trường dạng json trong Laravel.

      1. Bạn ơi cho m hỏi sao m chạy lệnh php artisan migrate nó lại báo lỗi nhỉ.
        Illuminate\Database\QueryException : SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right s
        yntax to use near ‘json not null default ‘{}’, `created_at` timestamp null, `updated_at` timestamp ‘ at line 1 (SQL: create table `roles` (`id` int unsigned not null auto_increment primary key, `name` varchar(191) n
        ot null, `slug` varchar(191) not null, `permissions` json not null default ‘{}’, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate ‘utf8mb4_unicode_ci’)

  3. Cho mình hỏi cái chỗ migrate roles.
    cái kiểu json nó hông hỗ trợ thì mình có cách nào khác để lưu permission hông

  4. Đỗ Đăng Phúc

    - Edit

    Reply

    Sao m chạy lệnh này mà ” php artisan make:policy PostPolicy –model=Post ” mà trong file PostPolicy nó ko giống của bạn nhỉ,

  5. Hi anh,
    Trước tiên thank anh vì bài viết rất chi tiết và cụ thể.
    Hiện tại đã đọc và hiểu hết tất cả cách hoạt động và làm tương đối thành công hết, chỉ riêng phần như update phải truyền vào 1 biến tên “post” nhưng khi qua postpolicy thì nó lại báo biến thứ 2 null.
    Em đã hỏi nhiều người nhưng vẫn k ai biết và nhận được câu trả lời.
    Link đăng: https://www.facebook.com/groups/vietnam.laravel/permalink/849087588814601/
    Thank tác giả.

Add Comment