Hướng dẫn role trong php

Tiếp tục series xây dựng project với PHP căn bản thì trong bài này mình sẽ viết thư viện xử lý phân quyền và kiểm tra trạng thái đăng nhập. Tại sao mình lại viết thành một thư viện? Tại vì tất cả các thao tác trong admin đều phải kiểm tra chức vụ của người dùng rồi mới tiến hành xử lý.

Hướng dẫn role trong php

Bài viết này được đăng tại freetuts.net, không được copy dưới mọi hình thức.

1. Viết thư viện phân quyền Role

Tất cả các hàm liên quan đến phân quyền mình sẽ đặt trong file libs/role.php. Bạn mở file này lên và nhập nội dung sau vào:

 

// Hàm thiết lập là đã đăng nhập
function set_logged($username, $level){
    session_set('ss_user_token', array(
        'username' => $username,
        'level' => $level
    ));
}

// Hàm thiết lập đăng xuất
function set_logout(){
    session_delete('ss_user_token');
}

// Hàm kiểm tra trạng thái người dùng đã đăng hập chưa
function is_logged(){
    $user = session_get('ss_user_token');
    return $user;
}

// Hàm kiểm tra có phải là admin hay không
function is_admin(){
    $user  = is_logged();
    if (!empty($user['level']) && $user['level'] == '1'){
        return true;
    }
    return false;
}

 

Bài viết này được đăng tại [free tuts .net]

Mình sẽ giải thích sơ lược các hàm trong thư viện.

Hàm thiết lập trạng thái đăng nhập:

 

function set_logged($username, $level){
    session_set('ss_user_token', array(
        'username' => $username,
        'level' => $level
    ));
}

 

Đối với mỗi người dùng chúng ta sẽ lưu hai thông tin là tên đăng nhập và cấp độ. Để lưu thông tin người dùng thì ta phải sử dụng đến thư viện Session lưu với key là ss_user_token.

Hàm thiết lập đăng xuất:

 

function set_logout(){
    session_delete('ss_user_token');
}

 

Để thiết lập đăng xuất ta chỉ việc sử dụng Session để xóa đi key ss_user_token.

Hàm kiểm tra trạng thái đăng nhập:

 

function is_logged(){
    $user = session_get('ss_user_token');
    return $user;
}

 

Để kiểm tra trạng thái đăng nhập thì ta sẽ kiểm tra giá trị Session của key ss_user_token có tồn tại hay không.

Hàm kiểm tra có phải là admin hay không:

 

function is_admin(){
    $user  = is_logged();
    if (!empty($user['level']) && $user['level'] == '1'){
        return true;
    }
    return false;
}

 

Để kiểm tra người dùng có phải là admin hay không thì ta kiểm tra level nếu bằng 1 là admin và ngược lại không phải là admin.

2. Lời kết

Như vậy là mình đã viết xong thư viện quản lý phân quyền và đăng nhập của người dùng. Bài tiếp theo chúng ta sẽ tìm hiểu về file bootstrap admin/index.php.

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.

Hướng dẫn role trong php
Hướng dẫn role trong php

  API Authentication trong Laravel-Vue SPA sử dụng Jwt-auth

  Các Laravel route tips giúp bạn cải thiện routing

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...
}

Policy

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

Đă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:



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();

        //
    }
}

Logic nghiệp vụ trong 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

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

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

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.

Route Model Binding

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.

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

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

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:

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

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

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?

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

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

Thực hành Laravel Authorization

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.

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.

Tạo database, dữ liệu test 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.

Hướng dẫn role trong php
Hướng dẫn role trong php

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.

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.

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

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

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:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.
Gate::resource('posts', 'PostPolicy');
0

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

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

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

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

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

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:

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

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:

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

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:

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

và databases\seeds\PostsSeeder.php:

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

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

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

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

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

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.

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

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

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

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

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

Đị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

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

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.

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

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:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Hướng dẫn role trong php
Hướng dẫn role trong php

Khi đăng nhập vào bằng user [email protected][email protected] 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.

Hướng dẫn role trong php
Hướng dẫn role trong php

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”.