Đăng bởi

Xây dựng API sử dụng Laravel và GraphQL

Chắc hẳn các bạn đã từng nghe về GraphQL GraphQL là một trong những đứa con của facebook được facebook tạo ra từ năm 2012 và được giới thiệu trước công chúng trong sự kiện React.js Conf 2015 nếu các bạn vẫn chưa biết hoặc muốn hiểu rõ hơn về GraphQL thì có thể tham khảo bài viết tổng quát GraphQL tại ĐÂY bây giờ chúng ta bắt đầu xây dựng một API sử dụng Laravel và GraphQL

Cài đặt và cấu hình GraphQL

Để cài đặt GraphQL chúng ta cần có sẳn một project Laravel sau đó mở file composer.json và thêm vào package sau :

    "require": {
        "folklore/graphql": "~1.0.0"
    },

Sau đó chạy lệnh :

$ composer install

Hoặc

$ composer update

Sau khi Composer update xong ta tiếp tục chạy lệnh :

$  php artisan

Đối với Laravel 5.X chúng ta thêm một service vào provider trong file *app/config/app.php *

Folklore\GraphQL\ServiceProvider::class

Và đăng ký một aliases :

'GraphQL' => Folklore\GraphQL\Support\Facades\GraphQL::class

Và chạy lệnh :

$ php artisan 

Nếu như các phương thức GraphQL xuất hiện như trong hình thì chúng ta đã cài đặt GraphQL thành công :Trong GraphQL gồm có 1 node root là viewer, gồm các lớp :

  • Connections: kết nối đến database
  • Felds: mô tả các fields trong node
  • Types: mô tả kiểu dữ liệu của field
  • Queries: mô tả cách truy vấn graph
  • Mutations: mô tả cách cập nhật graph

Bây giờ chúng ta bắt đầu vào việc tạo một UserType: để tạo UserType chúng ta chạy lệnh sau :

$ php artisan make:graphql:type UserType

Sau khi chạy lệnh thành sẽ xuất hiện một thư mục App một thư mục GraphQL/Type/UserType.php . Chúng ta bắt đầu mô tả các kiểu dữ liệu của User như sau:

class UserType extends BaseType
{
    protected $attributes = [
        'name' => 'User',
        'description' => 'A type'
    ];

    public function fields()
    {
        return [
            'id' => [
                'type' => Type::nonNull(Type::int())
            ],
            'name' => [
                'type' => Type::nonNull(Type::string())
            ],
            'email' => [
                'type' => Type::nonNull(Type::string())
            ],
            'created_at' => [
                'type' => Type::nonNull(Type::string())
            ],
            'updated_at' => [
                'type' => Type::nonNull(Type::string())
            ]
        ];
    }
}

Tiếp theo chúng ta tạo một UserQuery bằng cách chạy lệnh :

$ php artisan make:graphql:query UserQuery

Thư mục GraphQL/Query/UserQuery.php sẽ được tự động ra. tiếp tục chúng ta config như sau :

class UsersQuery extends Query {

    protected $attributes = [
        'name'  => 'User',
    ];

    public function type()
    {
        return Type::listOf(GraphQL::type('user'));
    }

    public function args()
    {
        return [
            'ids'   => [
                'name' => 'ids',
                'type' => Type::listOf(Type::int()),
            ],
        ];
    }

    public function resolve($root, $args, SelectFields $fields)
    {
        $select = $fields->getSelect();
        $with = $fields->getRelations();

        return User::where('id', '=', $args['id'])->with($with)->select($select)->get();
    }

}

Tiếp tục chúng ta tạo file Mutation bằng cách gõ lệnh :

$ php artisan make:graphql:mutation CreateUserMutation

Thư mục GraphQL/Mutation/CreateUserMutation.php sẽ được tự động ra. tiếp tục chúng ta config như sau :

class CreateUserMutation extends Mutation
{
    protected $attributes = [
        'name' => 'CreateUser',
        'description' => 'User mutation'
    ];

    public function type()
    {
        return GraphQL::type('User');
    }

    public function args()
    {
        return [
            'name' => [
                'type' => Type::nonNull(Type::string()),
                'rules' => ['required']
            ],
            'email' => [
                'type' => Type::nonNull(Type::string()),
                'rules' => ['required', 'email', 'unique:users']
            ],
            'password' => [
                'type' => Type::nonNull(Type::string()),
                'rules' => ['required', 'min:6']
            ]
        ];
    }

    public function resolve($root, $args, $context, ResolveInfo $info)
    {

        $user = new User();
        $user->fill($args);
        $user->password = bcrypt($args['password']);
        $user->save();
        return $user;
    }
}

Như vậy công việc tạo GraphQL đã hoàn tất bước cuối cùng là tiến thành config file config/graphql.php như sau :

 'schemas' => [
        'default' => [
            'query' => [
                'user' => App\GraphQL\Query\UserQuery::class,
            ],
            'mutation' => [
                'createUser' => App\GraphQL\Mutation\CreateUserMutation::class,
            ],
            // 'middleware' => []
        ],
    ],
    
    // The types available in the application. You can then access it from the
    // facade like this: GraphQL::type('user')
    //
    // Example:
    //
    'types' => [
        App\GraphQL\Type\UserType::class,
    ],

Như vầy chúng ta đã tiến hành xong các bước để thiết lập GraphQL bây giờ chúng ta truy cập Vào đường dẫn sau:

localhost/blog/public/graphiql

Giao diện xuất hiện:Đây chính là giao diện hiển thị của GraphQL . Bây giờ chúng ta sẽ bắt đầu viết một câu truy vấn đơn giản:

mutation {
    createUser (
        name: "test",
        email: "[email protected]",
        password: "test123"
    ){
        id,
        email
    }
}

Khi nhấn submit kết quả sẽ trả về như trong hình thì chúng ta đã thành công trong việc tạo truy vấn bằng GraphQL :Để truy ngược lại chúng ta làm như sau :

query {
    user(id: 1) {
        name,
        email
    }
}
Đăng bởi

Biến ứng dụng Laravel của bạn trở nên phức tạp hơn với gRPC

gRPC là gì ?

Chắc hẳn khi mới đọc tiêu đề, nhiều bạn thắc mắc tại sao lại biến ứng dụng đơn giản trở nên phức tạp để làm gì không biết, nhưng thực ra việc phức tạp này sẽ mang lại cho chúng ta rất nhiều lợi ích, với những ứng dụng nhỏ thì gRPC chưa thực sự cần thiết, tuy nhiên áp vào các ứng dụng lớn, cần được mở rộng trong tương lai, việc chuyển đổi từ REST sang gRPC đem lại hiệu quả rất lớn.

RPC

RPC, đó là chữ viết tắt của Remote Procedure Calls (tạm dịch là các cuộc gọi thủ tục từ xa), là một khái niệm nhằm cố gắng khái quát một lời gọi thủ tục thông thường trong trường hợp mà caller và receiver không cùng nằm trong một process – và được phân tán trên các máy riêng biệt. Việc này có ý nghĩa rất quan trọng vì trong các hệ thống phân tán (distributed system), application code ở nhiều server hơn là một server. Ví dụ thường thấy nhất chính là kiến trúc Microservice.

gRPC

Tối ưu cho việc “giao tiếp” giữa các server là lý do gRPC ra đời.

gRPC là một RPC framework gíup bạn kết nối giữa các service trong hệ thống, nó hỗ trợ load balancing, tracing, health checking và authentication, hỗ trợ từ ứng dụng mobile, trình duyệt cho tới back-end service, do Google phát triển.

Để giải bài toán trên, gRPC đã sử dụng binary để truyền đi thay vì phải encode chúng thành các ngôn ngữ trung gian JSON/XML. Việc này rõ ràng đã làm tăng tốc giao tiếp các servers lên rất nhiều, giảm overhead cho CPUs.

Thứ giúp gRPC giao tiếp binary ngon vậy chính là http/2, đây vốn là giao thức có rất nhiều cải tiến so với http/1.1

Sử dụng gRPC trong PHP

Không giống như Java, Go, hay Ruby,… PHP hiện tại chưa được chính Google hỗ trợ xây dựng SDK để dựng thành 1 gRPC server. Vì thế chúng ta muốn sử gRPC cho server thì cần thông qua 1 framework gọi là RoadRunner.

RoadRunner là 1 framework ở tầng infrastructure cho các ứng dụng PHP, nó được viết bằng Golang. Công việc của RoadRunner là chạy PHP dưới dạng các worker

Golang sẽ giúp RoadRunner chạy PHP app trên goroutine và hỗ trợ cân bằng tải trên các worker.

RoadRunner sẽ giữ các PHP worker luôn alive giữa các request, tránh việc tái khởi đọng lại app và tăng tốc cho các ứng dụng lớn. PHP worker được đặt trong resident memory, và luôn sẵn sàng cho request tiếp theo. RoadRunner còn sử dụng Goridge RPC sẽ giúp đẩy nhanh tốc độ load của ứng dụng lên server.

Cài đặt gRPC

Trước khi bắt đầu implement gRPC cho ứng dụng Laravel thì chúng ta setup môi trường development cho đủ các công cụ cần thiết

  • gRPC PHP extension
  • Google Protobuf
  • Google Protobuf compiler cho PHP server
  • Roadrunner

gRPC PHP extension

Việc cài thêm extension khá đơn giản, bạn chỉ cần sử dụng PECL và chạy lệnh

$ sudo pecl install grpc

Tuy nhiên nếu máy bạn có cài nhiều version PHP thì sẽ phức tạp hơn chút, mình đã gặp issue khi cài grpc là extension được build xong, khi sử dụng trong php.ini thì PHP không tìm thấy extension. Mình khắc phục bằng cách gỡ bỏ extension cũ và cài bằng lệnh này

$ sudo pecl php_suffix=7.4 install grpc 

thật (magic) PECL sẽ compile extension cho đúng phiên bản PHP mà bạn đang chọn là PHP 7.4

Cuối cùng bạn tìm file php.ini và thêm dòng này vào extension=grpc.so

Google Protobuf

Protocol buffer còn được biết như protobuf là language-neutral, platform-neutral của google phiên bản nội bộ được công bố vào năm 2001 và phiên bản công khai đầu tiên được giới thiệu vào năm 2008 ( Repository ), về cơ bản nó được sủ dụng để Serialized object, có vẻ nó khá giống XML hoặc JSON. Nó lưu trữ dữ liệu có cấu trúc có thể được Serialize hoặc De-Serialized tự động bưởi nhiều ngôn ngữ khác nhau. Nó được thiết kế để trở thành language/platform neutral và có thể mở rộng.

Việc cài protobuf cũng tương tự gRPC , bạn chạy command sau và rồi thêm dòng extension=protobuf.so vào php.ini

$ sudo pecl install protobuf

Google Protobuf compiler cho PHP server

Bởi vì gRPC chưa trực tiếp hỗ trợ các server viết bằng PHP nên với PHP chúng ta sử dụng 1 plugin để compile các file .proto cho PHP server

$ go get github.com/spiral/php-grpc/cmd/protoc-gen-php-grpc

để protoc có thể tìm thấy plugin mà bạn vừa kéo về thì bạn thêm vào file .zshrc (nếu đang dùng zsh) hoặc .bashrc 2 dòng này

export GO_PATH=~/go
export PATH=$PATH:/$GO_PATH/bin

đây là 1 pre-build binary để gen proto file, để sử dụng nó ta chỉ cần thêm plugin đó trong command compile ví dụ:

$ protoc --php_out=target-dir/ --php-grpc_out=target-dir/ sample.proto

Roadrunner

RoadRunner sẽ hỗ trợ bạn serve ứng dụng lên, tương tự nhưng artisan trong Laravel ý, bạn chỉ cần tải file rr-grpc về và để vào thư mục root của app

RoadRunner sẽ hỗ trợ bạn serve ứng dụng lên, tương tự nhưng artisan trong Laravel ý, bạn chỉ cần tải file rr-grpc về và để vào thư mục root của app

Implement gRPC server

Trong bài viết này mình sẽ implement gRPC cho phía server bằng PHP, và sử dụng luôn framework Laravel chọn xịn xò =))

Init project

Đầu tiền chúng ta cần init project

$ composer create-project laravel/laravel grpc-php-server

sau đó chúng ta cần cài thêm các package cần thiết như spiral/php-grpc, google/common-protos,… thành phẩm chúng ta sẽ có 1 file composer.json như này

{
    "name": "laravel/laravel",
    "type": "project",
    "description": "The Laravel Framework.",
    "keywords": ["framework", "laravel"],
    "license": "MIT",
    "require": {
        "php": "^7.4|^8.0",
        "ext-grpc": "^1.37",
        "fideloper/proxy": "^4.4",
        "fruitcake/laravel-cors": "^2.0",
        "google/common-protos": "^1.3",
        "google/protobuf": "^3.16",
        "grpc/grpc": "^1.36",
        "guzzlehttp/guzzle": "^7.0.1",
        "laravel/framework": "^8.12",
        "laravel/tinker": "^2.5",
        "nyholm/psr7": "^1.4",
        "spiral/php-grpc": "^v1.5.0",
        "spiral/roadrunner": "^1.9",
        "spiral/roadrunner-laravel": "^3.7",
        "ext-json": "*"
    },
    "require-dev": {
        "facade/ignition": "^2.5",
        "fakerphp/faker": "^1.9.1",
        "laravel/sail": "^1.0.1",
        "mockery/mockery": "^1.4.2",
        "nunomaduro/collision": "^5.0",
        "phpunit/phpunit": "^9.3.3",
        "spiral/dumper": "^1.1.7"
    },
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Database\\Factories\\": "database/factories/",
            "Database\\Seeders\\": "database/seeders/",
            "": "protos/generated/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    },
    "scripts": {
        "post-autoload-dump": [
            "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
            "@php artisan package:discover --ansi"
        ],
        "post-root-package-install": [
            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
        ],
        "post-create-project-cmd": [
            "@php artisan key:generate --ansi"
        ]
    },
    "extra": {
        "laravel": {
            "dont-discover": []
        }
    },
    "config": {
        "optimize-autoloader": true,
        "preferred-install": "dist",
        "sort-packages": true,
        "platform": {
            "php": "7.4.18"
        }
    },
    "minimum-stability": "dev",
    "prefer-stable": true
}

Trong file composer.json này mình có bổ sung thêm

"autoload": {
        "psr-4": {
            "App\\": "app/",
            "Database\\Factories\\": "database/factories/",
            "Database\\Seeders\\": "database/seeders/",
            "": "protos/generated/"
        }
    },

mục đích là để load thêm các file được compile từ Protobuf vào project.

Tạo auth.proto

Mục đích của file này là giúp define ra các service và cấu trúc request/response. File này sẽ được viết bằng Protobuf hay Protocols Buffer, là một ngôn ngữ dùng để mô tả các cấu trúc dữ liệu, chúng ta dùng protoc để biên dịch chúng thành mã nguồn của các ngôn ngữ lập trình khác nhau có chức năng serialize và deserialize các cấu trúc dữ liệu này thành dạng binary stream.

syntax = "proto3";

package protobuf.identity;

option php_metadata_namespace = "Protobuf\\Identity\\Metadata";

service AuthService {
    rpc SignIn (SignInRequest) returns (Response) {}
    rpc SignUp (SignUpRequest) returns (Response) {}
}

message SignInRequest {
    string email = 1;
    string password = 2;
}

message SignUpRequest {
    string name = 1;
    string email = 2;
    string password = 3;
    string password_confirmation = 4;
}

message Response {
    int64 id = 1;
    string token = 2;
}

Trong file này đơn giản chỉ định nghĩa service AuthService sẽ có 2 method rpc là SignIn và SignUp, chúng nhận vào tham số message được define theo cấu trúc ở bên dưới.

Sau khi đã tạo file auth.proto chúng ta cần phải compile chúng ra thành các file để sử dụng trong project, bằng command:

$  protoc --php_out=./protos/generated/ --php-grpc_out=./protos/generated/ ./protos/auth.proto

như vậy là chúng ta sẽ có file source code nằm trong thư mục protos/generated như sau: 

đây cũng là thư mục mà mình bổ sung vào trong file composer.json ở trên đấy

Implement logic cho AuthServiceInterface

Sau khi sử dụng protoc để có được file AuthServiceInterface, chúng ta đơn giản chỉ cần tiến hành implement logic cho các func trong service thôi

<?php

namespace App\Grpc\Controllers;

use Protobuf\Identity\AuthServiceInterface;
use Protobuf\Identity\Response;
use Protobuf\Identity\SignInRequest;
use App\Grpc\Contracts\Validator;
use Illuminate\Contracts\Hashing\Hasher;
use Spiral\GRPC\Exception\InvokeException;
use Spiral\GRPC\StatusCode;
use Protobuf\Identity\SignUpRequest;
use Throwable;
use Spiral\GRPC\ContextInterface;

class AuthController implements AuthServiceInterface
{
    /**
     * Input validator
     *
     * @var Validator
     */
    protected Validator $validator;

    /**
     * Hasher
     *
     * @var Hasher
     */
    protected Hasher $hasher;

    /**
     * Create new instance.
     *
     * @param  Validator $validator
     * @param  Hasher $hasher
     */
    public function __construct(Validator $validator, Hasher $hasher)
    {
        $this->validator = $validator;
        $this->hasher = $hasher;
    }

    /**
     * @param ContextInterface $ctx
     *
     * @param  SignUpRequest $request
     * @return Response
     * @throws Throwable
     */
    public function SignUp(ContextInterface $ctx, SignUpRequest $request): Response
    {
        $data = json_decode($request->serializeToJsonString(), true);

        $this->validator->validate($data, [
            'email' => 'bail|required|email',
            'name' => 'required|max:255',
            'password' => 'required|confirmed',
        ]);

        $response = new Response();

        $response->setId(1);
        $response->setToken("token"); //TODO use jwt to handle token base auth

        return $response;
    }

    /**
     * @param ContextInterface $ctx
     * @param SignInRequest $in
     * @return Response
     * @throws Throwable
     */
    public function SignIn(ContextInterface $ctx, SignInRequest $in): Response
    {
        $data = json_decode($in->serializeToJsonString(), true);

        $this->validator->validate($data, [
            'email' => 'required|email',
            'password' => 'required|min:6',
        ]);

        $response = new Response();

        $response->setId("1");
        $response->setToken("token"); //TODO using jwt to handle token base auth

        return $response;
    }
}

đơn giản vậy thôi là chúng ta đã xong phần logic cho các method rpc của app rồi

Implement PHP worker

Phần quan trọng nhất đây rồi.

Chúng ta sử dụng RoadRunner để serve các PHP worker lên và lắng nghe các rpc nên PHP worker là 1 thành phần quan trọng trong ứng dụng của bạn

<?php

use Spiral\RoadRunner\Worker;
use Spiral\Goridge\StreamRelay;

ini_set('display_errors', 'stderr');

require __DIR__ . '/vendor/autoload.php';

$app = require_once __DIR__ . '/bootstrap/app.php';

$app->singleton(
    App\Grpc\Contracts\Kernel::class,
    App\Grpc\Kernel::class
);

$app->singleton(
    App\Grpc\Contracts\ServiceInvoker::class,
    App\Grpc\LaravelServiceInvoker::class
);

$kernel = $app->make(App\Grpc\Contracts\Kernel::class);

$kernel->registerService(Protobuf\Identity\AuthServiceInterface::class);

$w = new Worker(new StreamRelay(STDIN, STDOUT));

$kernel->serve($w);

Khi đã có PHP worker rồi, chúng ta sẽ sử dụng rr-grpc đã tải về ở trên để serve app lên thôi. rr-grpc có yêu cầu file config như sau:

grpc:
  listen: "tcp://127.0.0.1:9001"  # Define host cho service
  proto: "protos/auth.proto"      # Define rõ file proto ở đâu
  workers:
    command: "php worker.php"      # Start worker từ file worker vừa tạo ở trên
    pool:                          # Cấu hình số lượng worker và số lượng job tối đa
      maxJobs: 1
      numWorkers: 1

Bây giờ chúng ta chạy command: ./rr-grpc -c .rr.yaml serve -v -d là xong 

server đã sẵn sàng lắng nghe các yêu cầu từ client rồi

Cấu trúc thư mục hoàn chỉnh

Tạm kết

Như vậy là chúng ta đã implement được phía server, do bài viết đã dài nên mình tạm kết ở đây, trong bài viết tới mình sẽ tiếp tục implement phía client, làm cách nào để client có thể giao tiếp với server thông qua gRPC.

Demo

Đăng bởi

Trở thành Senior PHP

Share cho a e dev PHP.

Để trở thành Senior PHP hãy nắm vững những skill sau:

– cache

– queue

– store file aws s3

– docker compose

– repository service

– nginx rewrite url, proxy pass

– Apache rewrite url

– swagger

– log rotate

– gitops

Bảo đảm lương 2k$.

Đăng bởi

Phát triển Blockchain với PHP

Ở bài viết này ilook sẽ hướng dẫn các Phát triển Blockchain thông qua ngôn ngữ PHP. Hy vọng bài viết sẽ ít nhiều nắm bắt và thực hiện được công nghệ blockchain

Blockchain hiện nay đang là xu hướng mới, đi đến đâu cũng blockchain, và hầu hết 1 số hội thảo công nghệ thì chủ đề blockchain luôn được nhiều các bạn lập trình viên cũng như các nhà quản lý doanh nghiệp đều quan tâm đến, ở bài viết này mình sẽ hướng dẫn các bạn viết 1 blockchain trong php.

Mình sẽ lướt qua khái niệm về blockchain theo cách mình hiểu 1 chút, để bạn nào chưa biết thì có thể hiểu qua về nó và bạn nào biết rồi thì cũng có thể đóng góp ý kiến của các bạn,

Vậy, đầu tiên blockchain là gì ? mà sao đi đâu cũng thấy nhắc đến blockchain? blockchain ra đời khi nào ? Blockchain dùng để làm gì? ứng dụng vào những cái việc gì? nghe thoáng qua ở đâu nói rằng blockchain là bitcoin, hô hố, có phải không nhỉ ?

Bạn tạo 1 thư mục blockchain trong thư mục webservice của bạn nhé, và tạo luôn 1 file index.php để chúng ta viết code trong đó nhé:

Ban đầu chúng ta sẽ khởi tạo 1 class có tên là Block, Class này sẽ làm nhiệm vụ mã hóa toàn bộ dữ liệu vào thành 1 chuỗi khóa, đoạn code sẽ như sau:

Class Block{
 
public $index; // biến này là index key của 1 block dữ liệu, ở ví dụ này của mình, mình sẽ dùng nó là index, nhưng nếu chúng ta áp dụng thực tế, thì chúng ta có thể coi nó như 1 khóa chính, 1 transaction id, để định danh cho 1 block dữ liệu
public $previousHash; // biến này này hash của block trước block này
public $timeHash; // biến này là thời gian mã hóa của block này
public $data; // biến này là data của block này
public $hash; // biến này chính là hash của block này
 
    public function __construct($index = 0, $previousHash = '', $timeHash = '', $data = '')
    {
 
        $this->index = $index;
        $this->previousHash = $previousHash;
        $this->timeHash = $timeHash;
        $this->data = $data;
        $this->hash = $this->execHash();
    }
 
    public function execHash()
    {
         
        if(is_array($this->data))
        {
            $data_content = json_encode($this->data);
        }else{
            $data_content = $this->data;
        }
         
        return hash('sha256', $this->index.$this->previousHash.$this->timeHash.$data_content);
    }
 
}

Chúng ta chuyển sang tạo Class Blockchain như sau:

Class BlockChain extends Block{
 
    public $chain = array();
 
    public function __construct()
    {
 
    }
 
}

Class Blockchain này sẽ thực hiện các chức năng như tạo mới 1 block, check xem blockchain này có bị chỉnh sửa hay chưa ?

Như bên trên các bạn cũng thấy, mình khởi tạo 1 class có tên Blockchian và thừa kế lại class Block bên trên, trong class Blockchain này hiện chưa có gì ngoài biến $chain đang được khai báo public và 1 hàm __construct(), việc tiếp theo chúng ta sẽ làm là tạo 1 hàm có chức năng khởi tạo block ban đầu, block ban đầu này cũng khá quan trọng, nó là 1 block ban đầu tạo ra hash, các block sau đó sẽ lấy hashkey của block này để thực hiện việc mã hóa của mình, vậy mình sẽ tạo hàm này trong class Blockchain

public function createGenesisBlock()
{
    return new Block(0,'hash_dau_tien', time(), 'data');
}

Tiếp theo chúng ta sẽ gọi hàm này trong hàm __construct để khi new Blockchain thì hàm createGenesisBlock sẽ được tự động gọi vào

Class BlockChain extends Block{
 
    public $chain = array();
 
    public function __construct()
    {
        $this->chain[] = $this->createGenesisBlock(); 
    }
}

Tiếp theo chúng ta tạo 1 hàm có tên getLatestBlock, hàm này sẽ có nhiệm vụ get ra block cuối cùng trong blockchain, và hàm này có nội dung như sau:

private function getLatestBlock()
    {
        return $this->chain[(count($this->chain) - 1)];
    }

Hàm này thì đơn giản rồi nên mình sẽ không giải thích thêm về hàm này và đi vào viết 1 hàm tiếp theo, hàm này vô cùng quan trọng, hàm này sẽ giúp chúng ta thêm vào block những dữ liệu mới

public function addaBlock($index, $timeHash, $data)
    {
        $previousHash = $this->getLatestBlock()->hash;
        $this->chain[] = new Block($index, $previousHash, $timeHash, $data);
    }

Ở hàm này, mình phải lấy ra được hash của block trước, lấy nó để làm gì ạ? thưa rằng bạn nhìn dòng thứ 2 trong hàm này bạn sẽ thấy Class Block cần truyền vào 1 biến có tên $previousHash, biến này khi thực hiện hash nó sẽ giống như 1 key trong hash, sau này cái $previousHash nó cũng là 1 thành phần để nói rằng block này có hợp lệ không ? block này có bị chỉnh sửa không ?

Okie, có vẻ gần hoàn chỉnh rồi, chúng ra review lại class BlockChain 1 chút nhé

Class BlockChain extends Block{
 
    public $chain = array();
 
    public function __construct()
    {
        $this->chain[] = $this->createGenesisBlock(); 
    }
 
    public function createGenesisBlock()
    {
        return new Block(0,'hash_dau_tien', time(), 'data');
    }
 
    private function getLatestBlock()
    {
        return $this->chain[(count($this->chain) - 1)];
    }
 
    public function addaBlock($index, $timeHash, $data)
    {
        $previousHash = $this->getLatestBlock()->hash;
        $this->chain[] = new Block($index, $previousHash, $timeHash, $data);
    }
 
     
}

Tổng cả class Block bên trên chúng ta sẽ có một code cơ bản và hoàn chỉnh

<?php
 
Class Block{
 
    public $index;
    public $previousHash;
    public $timeHash;
    public $data;
    public $hash;
 
    public function __construct($index = 0, $previousHash = '', $timeHash = '', $data = '')
    {
 
        $this->index = $index;
        $this->previousHash = $previousHash;
        $this->timeHash = $timeHash;
        $this->data = $data;
        $this->hash = $this->execHash();
    }
 
    public function execHash()
    {
         
        if(is_array($this->data))
        {
            $data_content = json_encode($this->data);
        }else{
            $data_content = $this->data;
        }
         
        return hash('sha256', $this->index.$this->previousHash.$this->timeHash.$data_content);
    }
 
}
 
Class BlockChain extends Block{
 
    public $chain = array();
 
    public function __construct()
    {
        $this->chain[] = $this->createGenesisBlock(); 
    }
 
    public function createGenesisBlock()
    {
        return new Block(0,'hash_dau_tien', time(), 'data');
    }
 
    private function getLatestBlock()
    {
        return $this->chain[(count($this->chain) - 1)];
    }
 
    public function addaBlock($index, $timeHash, $data)
    {
        $previousHash = $this->getLatestBlock()->hash;
        $this->chain[] = new Block($index, $previousHash, $timeHash, $data);
    }
 
}

Chúng ta sẽ thêm đoạn này vào cuối file index.php và chạy thử nó nhé

$bl = new BlockChain();
$bl->addaBlock(1,time(), 'Data thứ 2');
$bl->addaBlock(2,time(), 'Data thứ 3');
$bl->addaBlock(3,time(), 'Data thứ 4');

Sau khi chạy, chúng ta sẽ có kết quả như sau:

BlockChain Object
(
    [chain] => Array
        (
            [0] => Block Object
                (
                    [index] => 0
                    [previousHash] => ma_chuoi_dau_tien
                    [timeHash] => 1530436111
                    [data] => data cua toi can tao
                    [hash] => 1067948d9484bb8bf7dcf5a625db12521e8a4f395861dd93b0025aa7714417c6
                )
 
            [1] => Block Object
                (
                    [index] => 1
                    [previousHash] => 1067948d9484bb8bf7dcf5a625db12521e8a4f395861dd93b0025aa7714417c6
                    [timeHash] => 1530436111
                    [data] => Data thứ 2
                    [hash] => 3f4752159b07a36c2bef5feeda6f4eb25a0472e08d9f3ed629ff9b4293a1a776
                )
 
            [2] => Block Object
                (
                    [index] => 2
                    [previousHash] => 3f4752159b07a36c2bef5feeda6f4eb25a0472e08d9f3ed629ff9b4293a1a776
                    [timeHash] => 1530436111
                    [data] => Array
                        (
                            [transaction_id] => 12ewe1e12e12eqwd12e1
                            [content] => test
                        )
 
                    [hash] => be72872e39785b6ccaea102166d278e74ef10b9b7af066edead2a6107e6da27a
                )
 
            [3] => Block Object
                (
                    [index] => 3
                    [previousHash] => be72872e39785b6ccaea102166d278e74ef10b9b7af066edead2a6107e6da27a
                    [timeHash] => 1530436111
                    [data] => Data thứ 4
                    [hash] => 46af491628171e95276986c996e3f5c155642411bfefc7936fc4688e1d088f98
                )
 
        )
)

Như vậy là chúng ta đã tạo ra được blockchain với php, thật quá đơn giản phải không các bạn, ở bài sau mình sẽ hướng dẫn các bạn việc kiểm tra xem 1 trong các block trong blockchain đã bị chỉnh sửa hay chưa ?

Thêm phát kết là bitcoin chỉ là 1 loại tiền sử dụng công nghệ blockchain, chứ bitcoin không phải là blockchain, không thể lấy bitcoin làm tên gọi cho blockchain, các bạn đừng nhầm lẫn nhé, ai đang nhầm lẫn thì sửa lại luôn nhé. Hẹn gặp các bạn ở các bài tiếp theo

Đăng bởi

Sử dụng Pusher trong Laravel tạo thông báo realtime

1. Giới thiệu

Xin chào các bạn, hôm nay mình muốn chia sẻ về một chủ đề được sử dụng khá nhiều trong các project Laravel đó là thông báo realtime. Bất kì một tác vụ nào cần thông báo realtime như có comment mới, có lượt đăng ký mới, có like bài viết mới, … v.v chúng ta đều có thể sử dụng chức năng này.

Hiện tại Laravel đã hỗ trợ sẵn chúng ta làm việc này thông qua pusher Broadcast.

Vậy chi tiết như thế nào chúng ta cùng tìm hiểu nhé.

2. Tổng quan

Để tạo được ứng dụng sử dụng dịch vụ này, chúng ta cần:

  • Tạo view push notification lên pusher.com
  • Dựng 1 client lắng nghe notification được trả về từ pusher.com
  • Tạo 1 app trên web https://pusher.com/ để có 1 server lắng nghe client push message đến và trả về cho client khác.

3. Setup

** Ở đây mình giả định các bạn đã cài đặt project Laravel thành công. Bạn nào chưa biết cách cài đặt thì vô đây nha: https://laravel.com/docs/5.7/installation

3.1 Install pusher on laravel

Để dùng được pusher, chúng ta sẽ sử dụng package pusher/pusher-php-server, mở command line của bạn và chạy lệnh:

composer require pusher/pusher-php-server

3.2 Create route Laravel

Trong file này mình sẽ tạo 3 route đó là view để submit form push notification, 1 route để handle xử lý push data to pusher channel và 1 view để xem notification đã push. Dưới đây là nội dung file routes/web.php:

Route::get('/', function () {
    return view('showNotification');
});

Route::get('getPusher', function (){
   return view('form_pusher');
});

Route::get('/pusher', function(Illuminate\Http\Request $request) {
    event(new App\Events\HelloPusherEvent($request));
    return redirect('getPusher');
});

Trong 3 route trên:

  • Route / dùng để view notification được nhận từ pusher
  • Route /getPusher dùng để get view submit form
  • Route /pusher dùng để handle sự kiện submit form và push thông tin vào event pusher.

Tiếp đến chúng ta sẽ tạo view để send notification.

3.3 Create view send notification

Chúng ta sẽ tạo 1 file view để gửi data đến pusher thông qua submit form

Nội dung file view form_pusher.blade.php:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Demo Application</title>

</head>
<body>

<div class="container">
    <div class="col-md-4 col-md-offset-2">
        <fieldset style="padding-top: 30px">
            <legend>Form pusher:</legend>
            <form action="/pusher">
                <label for="">Message push</label>
                <br>
                <textarea name="contents" id="" cols="33" rows="10"></textarea>

                <br>
                <button class="btn btn-primary">Submit</button>
            </form>
        </fieldset>
    </div>
</div>
</body>
</html>

3.4 Create view receive, show notification

File này sẽ là file index để show notification khi có data từ pusher trả về.

Dưới đây là nội dung file showNotification.blade.php

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Demo Application</title>
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <style>.dropdown-container{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:200px;max-width:330px;margin:2px 0 0;list-style:none;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-container>.dropdown-menu{position:static;z-index:1000;float:none !important;padding:10px 0;margin:0;border:0;background:transparent;border-radius:0;-webkit-box-shadow:none;box-shadow:none;max-height:330px;overflow-y:auto}.dropdown-container>.dropdown-menu+.dropdown-menu{padding-top:0}.dropdown-menu>li>a{overflow:hidden;white-space:nowrap;word-wrap:normal;text-decoration:none;text-overflow:ellipsis;-o-text-overflow:ellipsis;-webkit-transition:none;-o-transition:none;transition:none}.dropdown-toggle{cursor:pointer}.dropdown-header{white-space:nowrap}.open>.dropdown-container>.dropdown-menu,.open>.dropdown-container{display:block}.dropdown-toolbar{padding-top:6px;padding-left:20px;padding-right:20px;padding-bottom:5px;background-color:#fff;border-bottom:1px solid rgba(0,0,0,0.15);border-radius:4px 4px 0 0}.dropdown-toolbar>.form-group{margin:5px -10px}.dropdown-toolbar .dropdown-toolbar-actions{float:right}.dropdown-toolbar .dropdown-toolbar-title{margin:0;font-size:14px}.dropdown-footer{padding:5px 20px;border-top:1px solid #ccc;border-top:1px solid rgba(0,0,0,0.15);border-radius:0 0 4px 4px}.anchor-block small{display:none}@media (min-width:992px){.anchor-block small{display:block;font-weight:normal;color:#777777}.dropdown-menu>li>a.anchor-block{padding-top:6px;padding-bottom:6px}}@media (min-width:992px){.dropdown.hoverable:hover>ul{display:block}}.dropdown-position-topright{top:auto;right:0;bottom:100%;left:auto;margin-bottom:2px}.dropdown-position-topleft{top:auto;right:auto;bottom:100%;left:0;margin-bottom:2px}.dropdown-position-bottomright{right:0;left:auto}.dropmenu-item-label{white-space:nowrap}.dropmenu-item-content{position:absolute;text-align:right;max-width:60px;right:20px;color:#777777;overflow:hidden;white-space:nowrap;word-wrap:normal;-o-text-overflow:ellipsis;text-overflow:ellipsis}small.dropmenu-item-content{line-height:20px}.dropdown-menu>li>a.dropmenu-item{position:relative;padding-right:66px}.dropdown-submenu .dropmenu-item-content{right:40px}.dropdown-menu>li.dropdown-submenu>a.dropmenu-item{padding-right:86px}.dropdown-inverse .dropdown-menu{background-color:rgba(0,0,0,0.8);border:1px solid rgba(0,0,0,0.9)}.dropdown-inverse .dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#2b2b2b}.dropdown-inverse .dropdown-menu>li>a{color:#cccccc}.dropdown-inverse .dropdown-menu>li>a:hover,.dropdown-inverse .dropdown-menu>li>a:focus{color:#fff;background-color:#262626}.dropdown-inverse .dropdown-menu>.active>a,.dropdown-inverse .dropdown-menu>.active>a:hover,.dropdown-inverse .dropdown-menu>.active>a:focus{color:#fff;background-color:#337ab7}.dropdown-inverse .dropdown-menu>.disabled>a,.dropdown-inverse .dropdown-menu>.disabled>a:hover,.dropdown-inverse .dropdown-menu>.disabled>a:focus{color:#777777}.dropdown-inverse .dropdown-header{color:#777777}.table>thead>tr>th.col-actions{padding-top:0;padding-bottom:0}.table>thead>tr>th.col-actions .dropdown-toggle{color:#777777}.notifications{list-style:none;padding:0}.notification{display:block;padding:9.6px 12px;border-width:0 0 1px 0;border-style:solid;border-color:#eeeeee;background-color:#fff;color:#333333;text-decoration:none}.notification:last-child{border-bottom:0}.notification:hover,.notification.active:hover{background-color:#f9f9f9;border-color:#eeeeee}.notification.active{background-color:#f4f4f4}a.notification:hover{text-decoration:none}.notification-title{font-size:15px;margin-bottom:0}.notification-desc{margin-bottom:0}.notification-meta{color:#777777}.dropdown-notifications>.dropdown-container,.dropdown-notifications>.dropdown-menu{width:450px;max-width:450px}.dropdown-notifications .dropdown-menu{padding:0}.dropdown-notifications .dropdown-toolbar,.dropdown-notifications .dropdown-footer{padding:9.6px 12px}.dropdown-notifications .dropdown-toolbar{background:#fff}.dropdown-notifications .dropdown-footer{background:#eeeeee}.notification-icon{margin-right:6.8775px}.notification-icon:after{position:absolute;content:attr(data-count);margin-left:-6.8775px;margin-top:-6.8775px;padding:0 4px;min-width:13.755px;height:13.755px;line-height:13.755px;background:red;border-radius:10px;color:#fff;text-align:center;vertical-align:middle;font-size:11.004px;font-weight:600;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}.notification .media-body{padding-top:5.6px}.btn-lg .notification-icon:after{margin-left:-8.253px;margin-top:-8.253px;min-width:16.506px;height:16.506px;line-height:16.506px;font-size:13.755px}.btn-xs .notification-icon:after{content:'';margin-left:-4.1265px;margin-top:-2.06325px;min-width:6.25227273px;height:6.25227273px;line-height:6.25227273px;padding:0}.btn-xs .notification-icon{margin-right:3.43875px}</style>
    <!--[if lt IE 9]>
    <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
    <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
</head>
<body>
<nav class="navbar navbar-inverse">
    <div class="container-fluid">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-9" aria-expanded="false">
                <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="#">Demo App</a>
        </div>

        <div class="collapse navbar-collapse">
            <ul class="nav navbar-nav">
                <li class="dropdown dropdown-notifications">
                    <a href="#notifications-panel" class="dropdown-toggle" data-toggle="dropdown">
                        <i data-count="0" class="glyphicon glyphicon-bell notification-icon"></i>
                    </a>

                    <div class="dropdown-container">
                        <div class="dropdown-toolbar">
                            <div class="dropdown-toolbar-actions">
                                <a href="#">Mark all as read</a>
                            </div>
                            <h3 class="dropdown-toolbar-title">Notifications (<span class="notif-count">0</span>)</h3>
                        </div>
                        <ul class="dropdown-menu">
                        </ul>
                        <div class="dropdown-footer text-center">
                            <a href="#">View All</a>
                        </div>
                    </div>
                </li>
                <li><a href="#">Timeline</a></li>
                <li><a href="#">Friends</a></li>
            </ul>
        </div>
    </div>
</nav>

<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="//js.pusher.com/3.1/pusher.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

<script type="text/javascript">
    var notificationsWrapper   = $('.dropdown-notifications');
    var notificationsToggle    = notificationsWrapper.find('a[data-toggle]');
    var notificationsCountElem = notificationsToggle.find('i[data-count]');
    var notificationsCount     = parseInt(notificationsCountElem.data('count'));
    var notifications          = notificationsWrapper.find('ul.dropdown-menu');

    if (notificationsCount <= 0) {
        notificationsWrapper.hide();
    }

    //Thay giá trị PUSHER_APP_KEY vào chỗ xxx này nhé
    var pusher = new Pusher('xxxxxxxxxxxxxxxxxxxxx', {
        encrypted: true,
        cluster: "ap1"
    });

    // Subscribe to the channel we specified in our Laravel Event
    var channel = pusher.subscribe('development');

    // Bind a function to a Event (the full Laravel class)
    channel.bind('App\\Events\\HelloPusherEvent', function(data) {
        var existingNotifications = notifications.html();
        var avatar = Math.floor(Math.random() * (71 - 20 + 1)) + 20;
        var newNotificationHtml = `
          <li class="notification active">
              <div class="media">
                <div class="media-left">
                  <div class="media-object">
                    <img src="https://api.adorable.io/avatars/71/`+avatar+`.png" class="img-circle" alt="50x50" style="width: 50px; height: 50px;">
                  </div>
                </div>
                <div class="media-body">
                  <strong class="notification-title">`+data.message+`</strong>
                  <!--p class="notification-desc">Extra description can go here</p-->
                  <div class="notification-meta">
                    <small class="timestamp">about a minute ago</small>
                  </div>
                </div>
              </div>
          </li>
        `;
        notifications.html(newNotificationHtml + existingNotifications);

        notificationsCount += 1;
        notificationsCountElem.attr('data-count', notificationsCount);
        notificationsWrapper.find('.notif-count').text(notificationsCount);
        notificationsWrapper.show();
    });
</script>
</body>
</html>

3.3 Create event for pusher

Dưới dây là nội dung file app/Events/HelloPusherEvent.php, file này đơn giản chỉ là lấy data từ form submit để push sang pusher

<?php
namespace App\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Http\Request;
use Illuminate\Queue\SerializesModels;

class HelloPusherEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $message;

    public function __construct(Request $request)
    {
        $this->message  = $request->contents;
    }

    public function broadcastOn()
    {
        return ['development'];
    }
}

4. Tạo app trên pusher.com

Các bạn truy cập vào web https://pusher.com/ đăng ký tài khoản sau đó đăng nhập và tạo app:

Create app on pusher

Sau khi tạo app các bạn vào xem detail channel mới tạo sẽ nhận được:

Thông tin channel pusher
app_id = "xxxxx"
key = "xxxxxxxxxxxxxxxxxxxxx"
secret = "xxxxxxxxxxxxxxxxxxxxxx"
cluster = "ap1"

và cập nhật lại file .env như sau:

BROADCAST_DRIVER=pusher

PUSHER_APP_ID=xxxxx
PUSHER_APP_KEY=xxxxxxxxxxxxxxxxxxxxx
PUSHER_APP_SECRET=xxxxxxxxxxxxxxxxxxxxx
PUSHER_APP_CLUSTER=ap1

5. Demo app

Mời các bạn theo dõi trong ảnh nhé 😃

Demo pusher laravel

6. Kết luận

Như vậy mình đã demo và hướng dẫn cho các bạn cách tạo dựng app để push notification realtime trên ứng dụng laravel. Các bạn có thể tự áp dụng notification trong project của mình rồi nhé 😄 Bài viết dự trên kinh nghiệm bản thân và tài liệu tham khảo, nếu có chỗ nào thắc mắc hay cần thảo luận các bạn vui lòng để lại comment bên dưới nhé.

Tài liệu tham khảo

  1. https://pusher.com/docs
  2. https://laravel.com/docs/5.7/broadcasting

Cảm ơn các bạn đã theo dõi bài viết của mình, hẹn gặp lại các bạn trong bài viết tiếp theo 😃.

Đăng bởi

Sử dụng Firebase với Laravel: Firebase Realtime Database

Giới Thiệu

Firebase là một hệ thống backend của google, cung cấp rất nhiều dịch vụ như:

  1. Real time database,
  2. Push notification,
  3. Firebase Analytics,
  4. Firebase Authentication,
  5. Firebase Cloud Messaging,
  6. Firebase Storage,
  7. Firebase Hosting,
  8. Firebase Crash reporting

ở bài hôm nay mình sẽ giới thiệu cách sử dụng firebase realtime database với Laravel

Setup Database

Đầu tiên hãy vào link này https://firebase.google.com/. Click vào Get Started.
Click vào Add project để tạo 1 project mới.

Chờ 1 lúc rồi click continue. Trình duyệt sẽ chuyển sang trang dashboard của firebase.

Ta vào mục Database để tạo 1 database mới

chọn Start in test mode

Sau đó vào mục Project Setting 

Vào tab Service Accounts rồi chọn Generate new private key

Ta sẽ được 1 file json có dạng như sau

{
  "type": "service_account",
  "project_id": "laravel-cefb7",
  "private_key_id": "4f9076071bef72d53d70fa402fd16cba4ac04721",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCZxYLAMhoDXkvK\n+3FEr4RABAPQRGE0mj8xdbMLnDTO5Ic2O1EmNUNrbZrva6wP7+W5e9aneoezR8t6\nFS+9T1Wskd37wQy3fB7Dm/qdGkvs27yAhnmUqsmBiPAkDGUm/VJG2JgXVNewjSOi\nQGFYIE7QsNkY1OyKcFyngd327FHJpKTWGrHr0x7OWeLIyk2OuZAc1+XwdD5wn+5M\n8tXEkieaUWpHguAXb+CHSHFnz2TfwSTa/4mAsZEYMikk/xzCW3TB0jtM8PGfT973\n5vZBleEqyD6YlA1kcIb9Sz0KNnJ5wqpmvcL6p1Aze4Nz9FW9CKi2dul0QPuaV6bM\nsWaMGrY7AgMBAAECggEAJZUV10aHgBJAYPxiWyCwqHqyuei0f0hBsKNtHqXPbE+D\nUnhd5YI4F54Cs4hz1bNSyKohNO6oEuO1sgFSmSi2Lka6Y+jzZDb0R+er8VC3hMzR\nvfHf7jtto3imBJF4+XYJcWJHjdBBJRfhwSqOAB1pVe0bKkwEAfPjWVdOuiZY+Pig\nrfy3T8lBH17Ud7K0ZlGcMmQs+PFvft0If/BrR2VhvZiXmIwzl5TZkkS68KipTBD/\n2+hzVUvBoaESnr7P04q2mF3SO5G461yVLB8zIZkCogDs1UIBIYlZ6LTPWTBjwXdi\n8lYLFfq0yqhPDkfdlwOmUkZd5jk4l0buyRkY50jT4QKBgQDYU5CBj4HHDRh4KOiR\nBUEpbE+/T8eyjTbBMm/NLdhfSOKrzI4EzNR1vi1n7b7WykVhpMqzkT1eN4dQPVad\nHHEHtnRm0/gmMP8CZrSzCA8IqN46KyMlQB4ZxVI8IDAEFhpqMqIsAfGrQzzh/Awv\n+rD83sZ+qsm2/ThDsPGAj95hIQKBgQC1+QUW9uAp/rWGK4KjkF3amk9oOdoY/Dca\n6ng96trpfXurvRJHN4RT8+DE+DJ0Y2zqfYHoU8ps87D4/BfKsDlMKLpNoTl77K5+\noGv2o8StrJIPY3qBjrOwao3IshcqcSDTBf2JLaGIsQg9Iv4yjA3Tn82hYb7TIUH4\n6XyIM6a/2wKBgQDVcra+L/RUjIZdYbgAB9hA4B9b3INlmVKylGqeYCMD+a226FIl\nSFpGh0zUJFDOLK0C6JAdWCzePxojwTIqObsJai02nOZYALZODzsy/7udxsnnSVMX\nWUmaEgnPS8806P5NVQKRO6XSX7i+Hm2EFe72w3XF3AzGotoWaIwSV2KMIQKBgBwe\necHdri/vEv/Dk3FQ6p/P/ns9jU0VFDLNjn0K4H76C/UyxzNICeOrKM1nCOzhPEBT\n2pIa+ImzcmJXGhoQ9xmkpX0/b5GNtj0M7wHytn6bzwoftht1AKE/Jd+gXWIKjybs\nowviiDaGHJ7N3HfVGoHcDLtmTkVzuy0Kb1rgmGDHAoGASB9h5Pd8jSY5h0QAANkV\nKlBm7OcGyUrcCglE73DGm+fv6B/+8t3Ma2ubwc8CQDgr22JyWdNr6jYW3CkRQ4N5\niDw5gQC+dvJ7TEymA1lRyoAKZZ6gkGJoWlGusK3STTmQbz+ysyYGurJKMpLKC4tK\npPgGcyOD0NiQ+no9IyDAKr0=\n-----END PRIVATE KEY-----\n",
  "client_email": "[email protected]",
  "client_id": "112140220979141836672",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-encv2%40laravel-cefb7.iam.gserviceaccount.com"
}

Bạn có thể lưu file này ở bất cứ chỗ nào trong project. Mình sẽ lưu ở trong thư mục controllers.

Tạo Controller

Đầu tiên, ta cài đặt package Kreait Firebase :

composer require kreait/firebase-php ^4.0

Sau đó ta tạo 1 controller

php artisan make:controller FirebaseController -r

Trong hàm __construct của controller ta thêm đoạn code sau

    protected $userRepository;
    protected $database;
    protected $firebase;

    public function __construct()
    {
        $serviceAccount = ServiceAccount::fromJsonFile(__DIR__ . '/laravel.json'); // đường dẫn của file json ta vừa tải phía trên
        $this->firebase           = (new Factory)
            ->withServiceAccount($serviceAccount)
            ->withDatabaseUri('https://{project-id}.firebaseio.com') //bạn có thẻ lấy project id ở mục project setting > general
            ->create();
        $this->database = $this->firebase->getDatabase();

        $this->userRepository = $this->database->getReference('user'); //lấy model user.
    }

Ok vậy là bạn đã có thể sử dụng firebase rồi đó.

1 số các câu lệnh cơ bản

Ordering Data

by key

$db->getReference('currencies')
    // order the reference's children by their key in ascending order
    ->orderByKey()
    ->getSnapshot();

by value: phải có index

{
    "currencies": {
        ".indexOn": ".value"
    }
}
$db->getReference('currencies')
    // order the reference's children by their value in ascending order
    ->orderByValue()
    ->getSnapshot();

by child: sắp xép theo child value, phải có index

{
    "currencies": {
        ".indexOn": ".value"
    }
}
$db->getReference('people')
    // order the reference's children by the values in the field 'height' in ascending order
    ->orderByChild('height')
    ->getSnapshot();

Filltering Data

để fillter được data trước tiên bạn phải chọn kiểu order data trước

limit to first: lấy 1 số lượng bản ghi đầu tiên

$db->getReference('people')
    // order the reference's children by the values in the field 'height'
    ->orderByChild('height')
    // limits the result to the first 10 children (in this case: the 10 shortest persons)
    // values for 'height')
    ->limitToFirst(10)
    ->getSnapshot();

limit to last: lấy 1 số lượng bản ghi cuối cùng

$db->getReference('people')
    // order the reference's children by the values in the field 'height'
    ->orderByChild('height')
    // limits the result to the last 10 children (in this case: the 10 tallest persons)
    ->limitToLast(10)
    ->getSnapshot();

equal to: giống như where = với vế trái được lấy từ order by

$db->getReference('people')
    // order the reference's children by the values in the field 'height'
    ->orderByChild('height')
    // returns all persons being exactly 1.98 (meters) tall
    ->equalTo(1.98)
    ->getSnapshot();

Saving Data

$postData = [...];
$postRef = $db->getReference('posts')->push($postData);

$postKey = $postRef->getKey(); // The key looks like this: -KVquJHezVLf-lSye6Qg

Update Data

$uid = 'some-user-id';
$postData = [
    'title' => 'My awesome post title',
    'body' => 'This text should be longer',
];

// Create a key for a new post
$newPostKey = $db->getReference('posts')->push()->getKey();

$updates = [
    'posts/'.$newPostKey => $postData,
    'user-posts/'.$uid.'/'.$newPostKey => $postData,
];

$db->getReference() // this is the root reference
   ->update($updates);

Delete Data

        $reference = 'posts/' . $id;
        $db->getReference($reference)->remove();

Kết Luận

Vậy là mình đã hướng dấn các bạn sử dụng dịch vụ Realtime Database của Firebase. Ở những bài sau mình sẽ hướng dẫn các bạn sử dụng các dịch vụ khác của Firebase.

Tài liệu tham khảo:
https://firebase-php.readthedocs.io/en/latest/

Đăng bởi

Xác thực API bằng OAuth 2 với Laravel Passport

Trong các ứng dụng phần mềm hiện đại, các web API là không thể thiếu, có rất nhiều các mô hình ứng dụng sử dụng web API như mô hình server-to-server, hay mô hình SPA (Single Page Application). Trong quá trình phát triển các API, rất cần thiết phải bảo vệ dữ liệu khỏi những con mắt tò mò, điều này với các hệ thống truyền thống rất đơn giản còn với API thì sao? Laravel tạo ra một gói thư viện Passport giúp thực hiện xác thực trong API đơn giản đơn giản hơn, nó cung cấp đầy đủ OAuth2. Laravel Passport được xây dựng dựa trên League OAuth2 server được phát triển bởi Alex Bilbie. Gợi ý: Cơ bản về OAuth 2, bạn nên đọc trước khi bắt đầu với Laravel Passport. Phương châm bài viết này cũng như các bài viết khác, chúng ta sẽ vừa học lý thuyết và thực hành cùng các ví dụ luôn, như vậy sẽ Hiểu sâu và nhớ lâu, vần vãi :)).

1. Cài đặt Laravel Passport

Chúng ta bắt đầu với việc cài đặt Laravel Passport thông qua Composer do gói thư viện này không được tích hợp sẵn cùng với framework Laravel.

c:\my-lapt>composer require laravel/passport
Using version ^2.0 for laravel/passport
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 12 installs, 0 updates, 0 removals
  - Installing phpseclib/phpseclib (2.0.5): Loading from cache
  - Installing psr/http-message (1.0.1): Loading from cache
...

Tiếp theo là những công việc chúng ta thường làm khi cài đặt một gói thư viện mới: Đăng ký provider trong config/app.php

'providers' => [
    ...
    Laravel\Passport\PassportServiceProvider::class,
    ...
],

Laravel Passport được thiết kế với một số các bảng trong cơ sở dữ liệu, bạn chỉ cần thực hiện lệnh artisan migrate. Passport đã đăng ký sẵn các bảng này trong framework, do đó bạn sẽ không tìm thấy các file tạo bảng trong thư mục database/migrates đâu.

c:\my-lapt>php artisan migrate
Migrating: 2016_06_01_000001_create_oauth_auth_codes_table
Migrated:  2016_06_01_000001_create_oauth_auth_codes_table
Migrating: 2016_06_01_000002_create_oauth_access_tokens_table
Migrated:  2016_06_01_000002_create_oauth_access_tokens_table
Migrating: 2016_06_01_000003_create_oauth_refresh_tokens_table
Migrated:  2016_06_01_000003_create_oauth_refresh_tokens_table
Migrating: 2016_06_01_000004_create_oauth_clients_table
Migrated:  2016_06_01_000004_create_oauth_clients_table
Migrating: 2016_06_01_000005_create_oauth_personal_access_clients_table
Migrated:  2016_06_01_000005_create_oauth_personal_access_clients_table

Ok, bước tiếp theo với câu lệnh artisan passport:install, sẽ tạo ra các khóa mã hóa dùng trong việc sinh ra các thẻ truy nhập (access token). Nó tạo ra hai khóa mã hóa là personal access và password grant.

c:\my-lapt>php artisan passport:install
Encryption keys generated successfully.
Personal access client created successfully.
Client ID: 1
Client Secret: erCzNy1k4G9rgHIc71xY9oyDfAoiEFX06w29audt
Password grant client created successfully.
Client ID: 2
Client Secret: xYedgLyFXXDuPXiZneBRqXW07nbHnLAGuvyoftfi

Các thông tin này bạn có thể lưu để dùng lại hoặc có thể lấy lại trong bảng oauth_clients trong database. Sau khi thực hiện câu lệnh này cần thêm trait Laravel\Passport\HasApiTokens và model App\User.php

<?php

namespace App;

use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

Tiếp đó gọi phương thức routes() trong phương thức boot() của app\Providers\AuthServiceProvider.php.

<?php

namespace App\Providers;

use Laravel\Passport\Passport;
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 = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

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

        Passport::routes();
    }
}

Cuối cùng là driver cho api trong config/auth.php

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],
],

2. Frontend được xây dựng sẵn trong Laravel Passport

Passport có sẵn các JSON API cho việc tạo các client (client id và client secret) và access token, tuy nhiên nó cũng hỗ trợ các giao diện fontend để làm việc này. Laravel Passport đã xây dựng sẵn một số Vue component sử dụng cho các công việc trong OAuth 2. Thưc hiện publish các component này bằng câu lệnh vendor:publish như sau:

c:\my-lapt>php artisan vendor:publish --tag=passport-components
Copied Directory [\vendor\laravel\passport\resources\assets\js\components] To [\
resources\assets\js\components\passport]
Publishing complete.

Các Vue component được publish sẽ nằm trong thư mục resources\assets\js\components. Muốn sử dụng chúng cần khai báo trong resources/assets/js/app.js là điểm bắt đầu của ứng dụng Vue.js như sau:

Vue.component(
    'passport-clients',
    require('./components/passport/Clients.vue')
);

Vue.component(
    'passport-authorized-clients',
    require('./components/passport/AuthorizedClients.vue')
);

Vue.component(
    'passport-personal-access-tokens',
    require('./components/passport/PersonalAccessTokens.vue')
);

Sau khi đăng ký, thực hiện biên dịch lại bằng Laravel Mix với câu lệnh npm run dev.

npm run dev

Tiếp đó, đưa các template vào bất kỳ đâu bạn muốn hiển thị các chức năng này.

<passport-clients></passport-clients>
<passport-authorized-clients></passport-authorized-clients>
<passport-personal-access-tokens></passport-personal-access-tokens>

3. Cài đặt môi trường thực hành

Tạm dừng lại lý thuyết, chúng ta sẽ cài đặt môi trường thực hành để thực hành với các dạng ủy quyền trong phần kế tiếp. Mô hình môi trường thực hành như sau:

Mô hình thực hành với OAuth 2 và Laravel Passport

Như trong mô hình OAuth 2, chúng ta thấy có 4 đối tượng, ở đây chúng ta sẽ xây dựng 2 hệ thống là Consumer.dev chính là Ứng dụng khách và Passport.dev là nơi cung cấp các dịch vụ API (bao gồm cả hai đối tượng: máy chủ ủy quyền và máy chủ tài nguyên). Ok, chúng ta sẽ tạo ra hai project Laravel có tên là consumer và passport, sau đó bạn có thể tạo các tên miền ảo cục bộ cho hai project này (Xem hướng dẫn tạo tên miền ảo cục bộ).

3.1 Tạo project passport.dev

  1. Tạo project sử dụng Laravel, tham khảo Cài đặt nhanh Laravel cho Windows -> composer new-project laravel/laravel my-lapt hoặc laravel new my-lapt
  2. Cài đặt gói laravel/passport -> composer require laravel/passport
  3. Đăng ký provider trong config/app.php.
  4. Migrate tạo bảng trong CSDL -> php artisan migrate, trước đó nhớ cấu hình kết nối database.
  5. Tạo client (client id và client secret) -> php artisan passport:install
  6. Thêm trait Laravel\Passport\HasApiTokens vào app\User.php
  7. Đăng ký route mặc định cho Laravel Passport trong AuthServiceProvider.php -> Passport::routes().
  8. Thiết lập driver cho api trong config/auth.php.
  9. Publish Vue component xây dựng sẵn -> php artisan vendor:publish –tag=passport-components.
  10. Khai báo các component vào app.js
  11. Biên dịch tài nguyên bằng Laravel Mix -> npm run dev (nếu tạo project mới chưa chạy npm install lần nào thì cần chạy lệnh npm install trước).
  12. Chạy Laravel Authentication -> php artisan make:auth, tạo user bằng Laravel Seeding -> php artisan make:seeder UsersTableSeeder, thêm code tạo user và chạy php artisan db:seed để đưa dữ liệu vào.
  13. Thêm các template vào view resources/views/home.blade.php để khi đăng nhập thấy các giao diện được cài đặt sẵn trong Laravel.

3.2 Tạo project consumer.dev

Tạo một project Laravel bình thường và cài đặt gói Guzzle HTTP bằng câu lệnh

composer require guzzlehttp/guzzle

Chú ý: Do quá trình tạo hai project này khá nhiều bước, bạn nào gặp vấn đề hãy comment ở dưới mình sẽ trả lời ngay khi có thể. Trong thời gian tới, mình sẽ quay lại màn hình phần này để upload lên Youtube giúp bạn thực hiện trực quan hơn. ## 4. Các dạng ủy quyền trong Laravel Passport

Chú ý: Bạn nên đọc Cơ bản về OAuth 2 để nắm được các thuật ngữ, khái niệm sử dụng trong bài.

4.1 Cấp ủy quyền bằng mã ủy quyền

Chúng ta cùng thực hiện các bước sử dụng trong cấp quyền bằng mã ủy quyền. Ví dụ như sau: Thực hiện một ví dụng giống như khi Tích hợp đăng nhập vào Facebook, hay Google, Twiter…, ở project consumer sẽ thực hiện tích hợp đăng nhập cấp bởi project passport.

Bước 1:

Vào http://passport.dev đăng ký ứng dụng (client bao gồm client_id và client_secret) giống như vào trang dành cho nhà phát triển của Facebook.

Giao diện trang passport có nút tạo mới client

Nhập thông tin đăng ký bao gồm: application name, application website, redirect uri (Giao diện xây dựng sẵn của Laravel bỏ qua application website, bạn có thể tự thêm vào).

Nhập thông tin đăng ký tạo mới client trong passport

Click vào Create, passport.dev sẽ tạo ra Client Id và Client Secret cho ứng dụng consumer.

Passport tạo Client Id và Client Secret cho ứng dụng của bạn

Bước 2: Xây dựng callback cho consumer.

Trong project Consumer, chúng ta tạo ra đường dẫn đăng nhập thông qua Passport là http://consumer.dev/auth/passport. Thêm route vào routes/web.php

use Illuminate\Http\Request;

Route::get('/auth/passport', function () {
     $query = http_build_query([
        'client_id' => '4',
        'redirect_uri' => 'http://consumer.dev/callback',
        'response_type' => 'code',
        'scope' => ''
      ]);

     return redirect('http://passport.dev/oauth/authorize?'.$query);
});

Route::get('/callback', function (Request $request) {
    $http = new GuzzleHttp\Client;

    $response = $http->post('http://passport.dev/oauth/token', [
        'form_params' => [
            'grant_type' => 'authorization_code',
            'client_id' => '4',
            'client_secret' => 'fuWkWtgITxQd0LqkrTFbZxkeCrtPoGnMndnp3kzJ',
            'redirect_uri' => 'http://consumer.dev/callback',
            'code' => $request->code
        ],
    ]);

    return json_decode((string) $response->getBody(), true);
});

Bước 3: Test

Bạn xem luồng thực hiện ủy quyền thông qua mã ủy quyền trong OAuth 2, ở đây chúng ta sẽ kiểm tra theo đúng các bước được đưa ra (13 bước).

1) Người dùng truy nhập ứng dụng consumer.dev

2) Consumer.dev hiển thị link “Đăng nhập bằng Passport” với đường dẫn là http://consumer.dev/auth/passport.

3) Người dùng nhấn vào đây, nó sẽ chuyển hướng đến request

https://passport.dev/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=

nhưng do yêu cầu này đòi hỏi phải xác thực nên nó chuyển hướng tiếp đến trang đăng nhập của Passport (chú ý, đây là đăng nhập của Passport.dev không phải của Consumer.dev do đó Consumer.dev không có thông tin về tài khoản người dùng). Giả sử chúng ta đã tạo tài khoản cho người dùng này là [email protected]/123456

4) Sau khi đăng nhập Passport.dev sẽ chuyển hướng về request ở trên và hiện ra thông báo người dùng cần ủy quyền cho ứng dụng Consumer.dev (Giống như khi thực hiện với Facebook, Google…)

Thông báo người dùng cần ủy quyền cho ứng dụng consumer

5) Người dùng ủy quyền cho ứng dụng khi bấm vào Authorize và Passport.dev chuyển hướng tiếp đến đường dẫn

http://consumer.dev/callback?code=AUTHORIZATION_CODE

6) Như vậy ứng dụng Consumer.dev đã nhập được mã ủy quyền và thực hiện POST đến đường dẫn

http://passport.dev/oauth/token?client_id=CLIENT_ID&client_secret=CLIENT_SECRET&
grant_type=authorization_code&code=AUTHORIZATION_CODE&redirect_uri=CALLBACK_URL

Chính là đoạn code này:

    $response = $http->post('http://passport.dev/oauth/token', [
        'form_params' => [
            'grant_type' => 'authorization_code',
            'client_id' => '4',
            'client_secret' => 'fuWkWtgITxQd0LqkrTFbZxkeCrtPoGnMndnp3kzJ',
            'redirect_uri' => 'http://consumer.dev/callback',
            'code' => $request->code
        ],
    ]);

7) Passport.dev kiểm tra thông tin Client Id, Client Secret kèm theo mã ủy quyền, nếu ok nó trả về access token và refresh token.

Passport trả về access token và refresh token cho consumer

8) Ở đây, consumer đã có được access token và refresh token do đó nó biết được người dùng đã xác thực.

9 – 13) Consumer muốn lấy thông tin về người dùng có thể thực hiện gửi GET đến http://passport.dev/api/user với header có dạng Authorization: Bearer AUTHORIZATION_CODE. Sửa lại routes/web.php trên Consumer như sau:

<?php

use Illuminate\Http\Request;

Route::get('/', function () {
     $query = http_build_query([
        'client_id' => '4',
        'redirect_uri' => 'http://consumer.dev/callback',
        'response_type' => 'code',
        'scope' => ''
      ]);

     return redirect('http://passport.dev/oauth/authorize?'.$query);
});

Route::get('/callback', function (Request $request) {
    $http = new GuzzleHttp\Client;

    $response = $http->post('http://passport.dev/oauth/token', [
        'form_params' => [
            'grant_type' => 'authorization_code',
            'client_id' => '4',
            'client_secret' => 'fuWkWtgITxQd0LqkrTFbZxkeCrtPoGnMndnp3kzJ',
            'redirect_uri' => 'http://consumer.dev/callback',
            'code' => $request->code,
        ],
    ]);

    $body = json_decode((string) $response->getBody(), true);

    $response = $http->get('http://passport.dev/api/user', [
      'headers' => [
            'Authorization' => 'Bearer ' . $body['access_token'],
        ],
    ]);

    return json_decode((string) $response->getBody(), true);
});

Chú ý, ở đây máy chủ ủy quyền và máy chủ tài nguyên cùng là một và chính là Passport.dev. Khi thực hiện lại bạn sẽ thấy Consumer.dev đã lấy được thông tin người dùng khi gửi access token đến máy chủ tài nguyên.

Lấy dữ liệu từ máy chủ tài nguyên bằng việc gửi access token

4.2 Ủy quyền ngầm định (Implicit Grant Token)

Ủy quyền ngầm định tương tự như ủy quyền thông qua mã ủy quyền, tuy nhiên access token được gửi trực tiếp về client mà không thông qua mã ủy quyền. Loại ủy quyền này thường được sử dụng trong Javascript và các ứng dụng mobile, nơi mà thông tin bí mật của client không thể lưu trữ bảo mật. Để sử dụng loại ủy quyền này, cần gọi đến phương thức enableImplicitGrant trong AuthServiceProvider:

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

    Passport::routes();

    Passport::enableImplicitGrant();
}

Khi loại ủy quyền này được kích hoạt, chúng ta có thể sử dụng client ID để yêu cầu access token bằng cách gửi request đến /oauth/authorize như sau:

Route::get('/redirect', function () {
    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://example.com/callback',
        'response_type' => 'token',
        'scope' => '',
    ]);

    return redirect('http://your-app.com/oauth/authorize?'.$query);
});

4.3 Ủy quyền theo thông tin người dùng (Resource Owner Password Credentials hay Password Grant)

OAuth 2 Resource Owner Password Credential (ủy quyền theo thông tin người dùng) cho phép các ứng dụng bên thứ nhất như các ứng dụng điện thoại có thể lấy access token sử dụng username/password. Nó tạo ra trực tiếp access token cho ứng dụng mà không cần sử dụng authorization code (mã ủy quyền). Trước khi ứng dụng có thể sinh ra access token thông qua ủy quyền theo thông tin người dùng, bạn cần tạo ra một client (client id và client secret). Sử dụng câu lệnh passport:client với tùy chọn –password. Nếu đã thực hiện câu lệnh passport:install ở bước cài đặt, bạn không cần chạy thêm lệnh trên.

php artisan passport:client --password

Khi đã tạo ra client, chúng ta có thể yêu cầu một access token thông qua một request POST đến đường dẫn /oauth/token cùng với username và password. Chú ý, route này đã được đăng ký bằng Passport::routes() trong AuthServiceProvider.php trong bước cài đặt, do đó không cần thêm route vào routes\api.php. Nếu request thành công, sẽ nhận được kết quả dạng JSON có chứa access token và refresh token.

$http = new GuzzleHttp\Client;

$response = $http->post('http://your-app.com/oauth/token', [
    'form_params' => [
        'grant_type' => 'password',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'username' => '[email protected]',
        'password' => 'my-password',
        'scope' => '',
    ],
]);

return json_decode((string) $response->getBody(), true);

Loại ủy quyền này thường sử dụng khi Consumer.dev là một thành viên trong hệ thống trong đó Passport.dev cũng là thành viên, tức là Consumer.dev đã là người trong nhà cùng với Passport.dev, do đó người dùng có thể gửi username/password đến cho Consumer.dev mà không ngần ngại. Thực tế, ví dụ bạn đã có một website có CSDL khách hàng, khi bạn tạo một website khác mà muốn sử dụng lại CSDL khách hàng này để đăng nhập bạn có thể sử dụng loại ủy quyền này. Bước 1: Để thực hành loại ủy quyền này, chúng ta tạo ra một form đăng nhập trên Consumer.dev.

Chú ý:

  1. Trên Passport cũng đã có form đăng nhập nhưng chúng ta muốn đăng nhập từ Consumer)
  2. Form đăng nhập trên Consumer không sử dụng Laravel Authentication là các tính năng xây dựng sẵn cho xác thực trong Laravel.

Tạo view resources/view/normal-login.blade.php với nội dung.

<html>
<head>
  <title>Allaravel Test</title>
  <!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
  <div class="wrapper">
    <div class="row">
      <div class="col-md-6 col-md-push-3">
        <div class="panel panel-default">
          <div class="panel-heading">
            <strong>Login</strong>
          </div>
          <div class="panel-body">
            <form action="http://consumer.dev/auth/normal" method="GET">
              <div class="form-group">
                <label>Email Address</label>
                <input class="form-control" placeholder="Enter your email address" type="text" v-model="login.email">
              </div>
              <div class="form-group">
                <label>Password</label>
                <input class="form-control" placeholder="Enter your email address" type="password" v-model="login.password">
              </div>
              <button class="btn btn-primary" type="submit">Login</button>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
</body>
</html>

Bước 2: Thêm route vào routes/web.php

Route::get('/login', function () {
  return view('normal-login');
});

Route::get('/auth/normal', function (Request $request) {
  $http = new GuzzleHttp\Client;

  $response = $http->post('http://passport.dev/oauth/token', [
      'form_params' => [
          'grant_type' => 'password',
          'client_id' => '2',
          'client_secret' => 'dWMEqIfKYZJHop71TxrnNs4EM1FOU3MSRUxNndPB',
          'username' => '[email protected]',
          'password' => '123456',
          'scope' => '',
      ],
  ]);

  return json_decode((string) $response->getBody(), true);
});

Chú ý, form đăng nhập này sẽ GET đến http://consumer.dev/auth/normal và xử lý tiếp bằng cách gửi POST đến Passport.dev.

Bước 3: Test

Khi vào http://consumer.dev/login, chúng ta có trang login, cần nhắc lại các chú ý để các bạn khỏi nhầm lẫn:

  1. Trên Passport cũng đã có form đăng nhập nhưng chúng ta muốn đăng nhập từ Consumer)
  2. Form đăng nhập trên Consumer không sử dụng Laravel Authentication là các tính năng xây dựng sẵn cho xác thực trong Laravel.

Trên consumer.dev có thể có database lưu các thông tin người dùng đăng nhập, tùy thuộc vào thiết kế ứng dụng của bạn. Thực hiện đăng nhập bằng tài khoản ở trên [email protected]/123456. Kết quả chúng ta sẽ thấy

Kết quả ủy quyền theo thông tin người dùng

Như vậy, đã bỏ qua bước gửi mã ủy quyền đúng như luồng thực hiện trong OAuth 2.

4.4 Ủy quyền theo thông tin ứng dụng (Client Credentials Grant Tokens)

Loại ủy quyền này phù hợp với các xác thực từ máy chủ đến máy chủ, ví dụ bạn sử dụng ủy quyền này trong các job được lập lịch, thực hiện các tác vụ bảo trì thông qua API. Để lấy được access token, chỉ cần gửi một request đến oauth/token:

$guzzle = new GuzzleHttp\Client;

$response = $guzzle->post('http://your-app.com/oauth/token', [
    'form_params' => [
        'grant_type' => 'client_credentials',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'scope' => 'your-scope',
    ],
]);

echo json_decode((string) $response->getBody(), true);

5. Một số vấn đề khác trong Laravel Passport

Như vậy, chúng ta đã thực hành với một số các loại ủy quyền trong Laravel Passport. Trong quá trình thực hiện, chúng thấy còn một số công việc khác chưa đi vào chi tiết, chúng ta sẽ cùng xem xét ở đây:

5.1 Thời gian hiệu lực của access token

Mặc định, Laravel Passport tạo ra các access token có thời gian hiệu lực dài, chúng ta không bao giờ yêu cầu tạo lại access token bằng refresh token, tuy nhiên trong thiết kế ứng dụng của bạn có thể cần thời gian hiêu lực ngắn, việc này hoàn toàn có thể thực hiện được khi bạn sử dụng các phương thức tokensExpireIn() và refreshTokensExpireIn() trong phương thức boot() của AuthServiceProvider:

use Carbon\Carbon;

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

    Passport::routes();

    Passport::tokensExpireIn(Carbon::now()->addDays(15));

    Passport::refreshTokensExpireIn(Carbon::now()->addDays(30));
}

5.2 Thêm, sửa, xóa Client

Laravel Passport cho phép tạo các Client (Client ID và Client Secret) thông qua câu lệnh artisan passport:client. Ngoài ra, có thể thực hiện được thông qua các JSON API đã được xây dựng sẵn. Trong các ví dụ dưới đây, sẽ sử dụng Axios là một HTTP client dạng Javascript, axios đã được đưa vào sẵn trong cấu hình Laravel (package.json), bạn có thể cài đặt bằng npm install.

5.2.1 Danh sách client

Gửi request GET đến oauth/clients

axios.get('/oauth/clients')
    .then(response => {
        console.log(response.data);
    });

5.2.2 Tạo mới một client

Gửi request POST đến oauth/clients

const data = {
    name: 'Client Name',
    redirect: 'http://example.com/callback'
};

axios.post('/oauth/clients', data)
    .then(response => {
        console.log(response.data);
    })
    .catch (response => {
        // List errors on response...
    });

Chú ý, name là tên của Client, khi Client được tạo ra nó sẽ sinh Client ID và Client Secret gửi lại trong response.

5.2.3 Cập nhật client

Gửi request PUT đến oauth/clients/{client_id}

const data = {
    name: 'New Client Name',
    redirect: 'http://example.com/callback'
};

axios.put('/oauth/clients/' + clientId, data)
    .then(response => {
        console.log(response.data);
    })
    .catch (response => {
        // List errors on response...
    });

5.2.4 Xóa client

Gửi request DELETE đến oauth/clients/{client_id}

axios.delete('/oauth/clients/' + clientId)
    .then(response => {
        //
    });

5.3 Tạo access token

5.3.1 Tạo Personal Access Client

Trước khi tạo ra access token, bạn cần tạo Personal Access Client là một khóa mật mã được sử dụng trong quá trình tạo các access token. Để tạo ra Personal Access Token bạn có thể sử dụng lệnh

php artisan passport:client --personal

Hoặc nếu đã chạy câu lệnh

php artisan passport:install

trong phần cài đặt rồi thì thôi.

5.3.2 Quản lý Personal Access Token

Sử dụng phương thức createToken() của Model User để tạo ra access token

$user = App\User::find(1);

// Creating a token without scopes...
$token = $user->createToken('Token Name')->accessToken;

// Creating a token with scopes...
$token = $user->createToken('My Token', ['place-orders'])->accessToken;

5.4 Bắt buộc xác thực API

Các route cho API được khai báo trong routes/api.php (từ Laravel 5.3 các route được tách biệt từ app\Http\route.php về các file web.php, api.php trong thư mục routes). Để bắt buộc phải xác thực với các API giúp bảo vệ dữ liệu khỏi con mắt tò mò, chúng ta chỉ cần sử dụng Middleware auth:api cho các route này:

Route::get('/user', function () {
    //
})->middleware('auth:api');

6. Lời kết

Cuối cùng thì cũng đã kết thúc, tổng kết lại bạn nắm được hai vấn đề chính là hiểu chi tiết hơn về OAuth 2 sau khi học Lý thuyết OAuth 2 và cách sử dụng Laravel Passport để tạo ra các hệ thống hỗ trợ OAuth 2. Bài viết được tổng hợp từ nhiều nguồn kết hợp với kinh nghiệm thực tế, nhưng không tránh khỏi thiếu sót, bạn hãy cùng thảo luận với chúng tôi ở comment dưới đây nhá.