Xác thực tài khoản mới đăng ký bằng email trong Laravel

Trong một hệ thống website các tính năng liên quan đến người dùng là rất quan trọng bao gồm có cả xác thực người dùng. Chúng ta đã được tìm hiểu cách các công đoạn trong xác thực người dùng như đăng ký người dùng mới, đăng nhập… trong bài Xác thực người dùng trong Laravel thật đơn giản. Tuy nhiên, đây là những gì có sẵn trong laravel do đó có nhiều tình huống phức tạp ở thực tế chưa đáp ứng được. Trong ví dụ được đưa ra ở bài viết trên, nếu người dùng đăng ký bằng một email không có thật, hệ thống vẫn cho phép, tuy nhiên chúng ta muốn người dùng phải nhập vào một tài khoản email thật để có thể sử dụng trao đổi thông tin sau này. Có một cách thức rất nhiều các website hiện tại đang sử dụng là gửi đến email đăng ký một đường dẫn chứa mã xác thực, nếu người đăng ký không mở email và kích hoạt mã xác thực này thì tài khoản vẫn chưa thể sử dụng được.

Luồng xử lý xác thực người dùng bằng mã xác thực qua email

Triển khai mã xác thực qua email trong Laravel

Bước 1: Tạo thêm trường lưu thông tin xác thực trong database

Đầu tiên chúng ta tạo một bảng mới user_activations chứa mã xác thực, tạo một file migration (Xem Laravel Migration).

c:\xampp\htdocs\laravel-test>php artisan make:migration create_user_activations_
table --create=user_activations
Created Migration: 2017_04_21_092239_create_user_activations_table

Chúng ta thấy file 2017_04_21_092239_create_user_activations_table đã được tạo ra trong database/migrations với các hàm được tạo sẵn, thêm nội dung tạo các trường khác cho bảng user_activations.

<?php

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

class CreateUserActivationsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('user_activations', function (Blueprint $table) {
            $table->integer('user_id')->unsigned();
            $table->string('activation_code')->index();
            $table->timestamps();
        });
    }

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

Tại sao chúng ta không đưa trường activation_code vào trong bảng user cho nhanh chóng? vấn đề ở chỗ nếu bảng user có hàng triệu bản ghi, khi đó thông tin activation_code sẽ làm ảnh hưởng đến hiệu năng bảng user. Tạo một file migration nữa để thêm trường active vào bảng users:

c:\xampp\htdocs\laravel-test>php artisan make:migration alter_users_table --tabl
e=users
Created Migration: 2017_04_21_093448_alter_users_table

Ok, thay đổi nội dung file migration vừa tạo ra:

<?php

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

class AlterUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->boolean('active')->default(false);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('active');
        });
    }
}

ok, tiếp theo chúng ta thực hiện các migration này trên database bằng lệnh artisan migrate

c:\xampp\htdocs\laravel-test>php artisan migrate
Migrated: 2017_04_21_092239_create_user_activations_table
Migrated: 2017_04_21_093448_alter_users_table

Bạn mở database ra và xem các thay đổi trên cơ sở dữ liệu laravel-test.

Bước 2: tạo model UserActivation để quản lý các hành động trên database

Tạo Model bằng lệnh artisan make:model

c:\xampp\htdocs\laravel-test>php artisan make:model UserActivation
Model created successfully.

Thay đổi nội dung của model này như sau (app\UserActivation.php):

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class UserActivation extends Model
{
    protected $table = 'user_activations';

    protected function getToken()
    {
        return hash_hmac('sha256', str_random(40), config('app.key'));
    }

    public function createActivation($user)
    {

        $activation = $this->getActivation($user);

        if (!$activation) {
            return $this->createToken($user);
        }
        return $this->regenerateToken($user);

    }

    private function regenerateToken($user)
    {

        $token = $this->getToken();
        UserActivation::where('user_id', $user->id)->update([
            'token' => $token,
            'created_at' => new Carbon()
        ]);
        return $token;
    }

    private function createToken($user)
    {
        $token = $this->getToken();
        UserActivation::insert([
            'user_id' => $user->id,
            'token' => $token,
            'created_at' => new Carbon()
        ]);
        return $token;
    }

    public function getActivation($user)
    {
        return UserActivation::where('user_id', $user->id)->first();
    }


    public function getActivationByToken($token)
    {
        return UserActivation::where('token', $token)->first();
    }

    public function deleteActivation($token)
    {
        UserActivation::where('token', $token)->delete();
    }
}

Bước 3: Định nghĩa route cho xử lý xác thực

Thêm route vào routes/web.php để xử lý đường dẫn xác thực trong email.

Route::get('user/activation/{token}', 'Auth\RegisterController@activateUser')->name('user.activate');

Bước 4: Tạo lớp Mailable và view để xử lý việc gửi mail.

Laravel Mailable được sử dụng trong việc gửi email (Xem thêm Gửi nhận email bằng Laravel Mail). Chú ý, nếu dùng các dịch vụ email Mailgun và SparkPost thì cần cài đặt thêm gói Guzzle HTTP bằng câu lệnh composer:

composer require guzzlehttp/guzzle

Bài viết này sẽ sử dụng Gmail (trong các ví dụ trước cũng đã cài đặt Guzzle HTTP sẵn trong source code của laravel-test rồi, nếu bạn nào thực hiện các ví dụ từ đầu thì cũng đã cài đặt).

Tạo lớp Mailable có tên UserActivationEmail bằng câu lệnh artisan make:mail UserActivationEmail.

c:\xampp\htdocs\laravel-test>php artisan make:mail UserActivationEmail
Mail created successfully.

Câu lệnh này sẽ tạo ra file UserActivationEmail.php trong app\Mail, sửa đổi lại nội dung file như sau:

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;

class UserActivationEmail extends Mailable
{
    use Queueable, SerializesModels;
    protected $user;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct($user)
    {
        $this->user = $user;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->view('email.user-activation')->with('user', $this->user);
    }
}

Chúng ta sẽ truyền $user vào view để hiển thị các thông tin người dùng đăng ký trong nội dung email xác thực. Tạo view user-activation.blade.php trong resouces/views/email.

<!DOCTYPE html>
<html>
<head>
	<title>Activation Email - Allaravel.com</title>
</head>
<body>
	<p>
		Chào mừng {{ $user->name }} đã đăng ký thành viên tại Allaravel.com. Bạn hãy click vào đường link sau đây để hoàn tất việc đăng ký.
		</br>
		<a href="{{ $user->activation_link }}">{{ $user->activation_link }}</a>
	</p>
</body>
</html>

Bước 5: Hoàn tất xác thực người dùng bằng mã xác thực trong email

Các bước từ 1 đến 4 đã chuẩn bị tương đối các việc cần làm từ tạo bảng, thêm thông tin quản trị trên database đến tạo các lớp gửi nhận email, lớp xử lý mã xác thực và định nghĩa route cho xử lý mã xác thực. Tiếp theo chúng ta sẽ hoàn tất các công việc còn lại. Chúng ta bắt đầu với app\Http\Controllers\Auth\RegisterController.php đây là nơi các hàm xử lý việc đăng ký diễn ra.

Trước hết chúng ta tạo một class ActivationService.php trong app\Classes để xử lý gửi nhận email, xử lý trường active… và tiện dùng cho các trường hợp cần xử lý xác thực sau này.

<?php
namespace App\Classes;

use Mail;
use App\UserActivation;
use App\Mail\UserActivationEmail;
use App\User;

class ActivationService
{
    protected $resendAfter = 24; // Sẽ gửi lại mã xác thực sau 24h nếu thực hiện sendActivationMail()
    protected $userActivation;

    public function __construct(UserActivation $userActivation)
    {
        $this->userActivation = $userActivation;
    }

    public function sendActivationMail($user)
    {
        if ($user->activated || !$this->shouldSend($user)) return;
        $token = $this->userActivation->createActivation($user);
        $user->activation_link = route('user.activate', $token);
        $mailable = new UserActivationEmail($user);
        Mail::to($user->email)->send($mailable);
    }

    public function activateUser($token)
    {
        $activation = $this->userActivation->getActivationByToken($token);
        if ($activation === null) return null;
        $user = User::find($activation->user_id);
        $user->active = true;
        $user->save();
        $this->userActivation->deleteActivation($token);

        return $user;
    }

    private function shouldSend($user)
    {
        $activation = $this->userActivation->getActivation($user);
        return $activation === null || strtotime($activation->created_at) + 60 * 60 * $this->resendAfter < time();
    }

}

Thực hiện ghi đè hàm register của trail Illuminate\Foundation\Auth\RegistersUsers được sử dụng bởi RegisterController bằng cách thêm phương thức register vào RegisterController.php:

<?php

namespace App\Http\Controllers\Auth;

use App\User;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Auth\RegistersUsers;
use App\Jobs\SendWelcomeEmail;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Auth\Events\Registered;
use App\Classes\ActivationService;

class RegisterController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Register Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles the registration of new users as well as their
    | validation and creation. By default this controller uses a trait to
    | provide this functionality without requiring any additional code.
    |
    */

    use RegistersUsers;

    /**
     * Where to redirect users after registration.
     *
     * @var string
     */
    protected $redirectTo = '/login';

    protected $activationService;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct(ActivationService $activationService)
    {
        $this->middleware('guest');
        $this->activationService = $activationService;
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array  $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name'                 => 'required|max:255',
            'email'                => 'required|email|max:255|unique:users',
            'password'             => 'required|min:6|confirmed',
            'g-recaptcha-response' => 'required|recaptcha'
        ]);
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param  array  $data
     * @return User
     */
    protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
        ]);
    }

    /**
     * Handle a registration request for the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function register(Request $request)
    {
        $this->validator($request->all())->validate();
        
        //event(new Registered($user = $this->create($request->all())));
        //$this->guard()->login($user);
        //return $this->registered($request, $user)?: redirect($this->redirectPath());

        $user = $this->create($request->all());
        event(new Registered($user));
        //$this->guard()->login($user);

        $this->activationService->sendActivationMail($user);
        
        return redirect('/login')->with('status', 'Bạn hãy kiểm tra email và thực hiện xác thực theo hướng dẫn.');
    }

    public function activateUser($token)
    {
        if ($user = $this->activationService->activateUser($token)) {
            auth()->login($user);
            return redirect('/login');
        }
        abort(404);
    }
}

Tiếp theo, chúng ta sẽ xử lý nếu tài khoản mới đăng ký chưa được active mà thực hiện đăng nhập thì sẽ cảnh báo phải kiểm tra email và thực hiện xác thực theo hướng dẫn. Để làm việc này, thực hiện ghi đè phương thức authenticated trong Trail Illuminate\Foundation\Auth\AuthenticatesUsers ở LoginController (app\Http\Controllers\Auth\LoginController.php).

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use App\Classes\ActivationService;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = '/';
    protected $activationService;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct(ActivationService $activationService)
    {
        $this->middleware('guest', ['except' => 'logout']);
        $this->activationService = $activationService;
    }

    /**
     * The user has been authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  mixed  $user
     * @return mixed
     */
    protected function authenticated(Request $request, $user)
    {
        if (!$user->active) {
            $this->activationService->sendActivationMail($user);
            auth()->logout();
            return back()->with('warning', 'Bạn cần xác thực tài khoản, chúng tôi đã gửi mã xác thực vào email của bạn, hãy kiểm tra và làm theo hướng dẫn.');
        }
        return redirect()->intended($this->redirectPath());
    }
}

Sau đó, chúng ta cần thay đổi view login trong resources/views/auth/login.blade.php để hiển thị thông báo khi chưa thực hiện xác thực qua email.

@extends('layouts.default')

@section('title', 'Đăng nhập - Allaravel.com')

@section('content')
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Đăng nhập</div>
                <div class="panel-body">
                    @if (session('warning'))
                        <span class="alert alert-warning help-block">
                            <strong>{{ session('warning') }}</strong>
                        </span>
                    @endif                    

                    <form class="form-horizontal" role="form" method="POST" action="{{ route('login') }}">
                        {{ csrf_field() }}

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

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

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

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

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

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

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

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

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

Bước 6: Kiểm tra kết quả

Vào trang đăng ký thành viên http://laravel.dev/register

Màn hình đăng ký tài khoản mới trong Laravel Dev

Điền các thông tin đăng ký thành viên mới, click vào I’m not a robot để hoàn thành Google reCAPTCHA (Xem Tích hợp Google reCAPTCHA vào ứng dụng Laravel) và nhấn Đăng ký. Sau khi đăng ký xong, tài khoản đã được lưu vào database nhưng ở dạng unactive. Thử đăng nhập xem thế nào.

Thông báo xác thực tài khoản khi đăng nhập

Thực hiện kiểm tra email và click vào đường link xác thực.

Email xác thực gửi đến thành viên vừa đăng ký

Sau khi click vào đường link xác thực này, bản ghi user trong bảng users sẽ chuyển trường active từ 0 sang 1 và người dùng mới này có thể đăng nhập hệ thống.

2 thoughts on “Xác thực tài khoản mới đăng ký bằng email trong Laravel

  1. Bị nhầm mấy đoạn, trong DB thì ghi là activation_code còn trong code thì lại ghi là token -> xung đột.
    Code chạy mượt lắm, ths

Add Comment