michaeldzjap/twofactor-auth

适用于Laravel的消息鸟验证的两因素认证包

v3.4.0 2024-09-01 18:06 UTC

README

Latest Stable Version Total Downloads Latest Unstable Version StyleCI License

laravel-two-factor-authentication

适用于Laravel >= 8的两因素认证包(对于Laravel 5到7,您需要此包的1或2版本)

目录

描述

这是一个适用于Laravel的两因素认证包。它受到Laravel Two-Factor Authentication包的极大启发。本包与上述包的主要区别在于

  • 此包目前仅适用于MessageBird Verify api或'null'驱动程序,该驱动程序在两因素认证过程中执行所有步骤而不进行实际验证。这可以用于测试目的。但是,您可以自己指定自定义提供者。
  • 此包使用节流来限制一定时间内失败认证尝试的数量。
  • 此包的当前版本仅保证与Laravel >= 8兼容。此包的2.*版本适用于Laravel 5.5到7。此包的1.*版本适用于Laravel 5.4。尚未测试Laravel 5.4之前版本。

重要

从Laravel 5.8开始,默认使用bigIncrements而不是increments作为users表上的id列。因此,此包的默认值是使用相同的约定来使用two_factor_auths表上的user_id列。如果您不希望这样做,您可以通过修改此包发布的迁移文件来根据您的喜好更改此约定。

发布此包的迁移文件可以提供更多的灵活性,以自定义数据库结构。然而,如果已经运行了作为安装此包的早期版本的一部分的迁移,它也可能导致问题。在这种情况下,您可能只想跳过再次运行迁移,或者在特定环境中运行迁移。在这里,Schema::hasColumn()Schema::hasTable()方法可能很有用。

可选修正

此包在v2.3.0之前的版本中,错误地使用increments而不是unsignedInteger创建two_factor_auths表上的user_id列。实际上,这个错误无关紧要。虽然没有必要为user_id列设置primary键,但它也不会引起任何问题。但是,如果出于某种原因您不喜欢这种想法,可以使用以下形式的迁移安全地删除primary

<?php

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

class RemovePrimaryFromTwoFactorAuthsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('two_factor_auths', function (Blueprint $table) {
            $table->dropForeign(['user_id']);
        });

        Schema::table('two_factor_auths', function (Blueprint $table) {
            $table->unsignedInteger('user_id')->change();
            $table->dropPrimary(['user_id']);
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('two_factor_auths', function (Blueprint $table) {
            $table->dropForeign(['user_id']);
        });

        Schema::table('two_factor_auths', function (Blueprint $table) {
            $table->increments('user_id')->change();
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        });
    }
}

请注意,您需要doctrine/dbal包才能使此迁移工作。此外,如果您的users表上的id列是bigIncrements类型,您必须将$table->unsignedInteger('user_id')->change();行更改为$table->unsignedBigInteger('user_id')->change();,并将$table->increments('user_id')->change();行更改为$table->bigIncrements('user_id')->change();

安装

  1. 使用Composer安装,请运行
composer require michaeldzjap/twofactor-auth

如果您想使用MessageBird Verify作为双因素认证提供商,那么您还需要安装MessageBird PHP API

composer require messagebird/php-rest-api

别忘了将您的MESSAGEBIRD_ACCESS_KEYTWO_FACTOR_AUTH_DRIVER=messagebird变量添加到.env文件中。如果您想使用默认的'null'驱动程序,那么请勿在.env中定义TWO_FACTOR_AUTH_DRIVER变量。

从Laravel 7版本开始,您还需要安装laravel/ui

composer require laravel/ui
  1. 将服务提供程序添加到config/app.php中的'providers'数组中
MichaelDzjap\TwoFactorAuth\TwoFactorAuthServiceProvider::class
  1. 运行以下artisan命令以发布配置、语言和视图文件
php artisan vendor:publish

如果您只想发布这些文件组中的一个,例如,如果您不需要视图或语言文件,可以将以下命令之一追加到artisan命令中:--tag=config--tag=lang--tag-views

  1. 重要:请确保在运行此包的任何迁移之前执行此步骤,否则可能会得到意外结果。

从Laravel 5.8版本开始,默认情况下,在users表的id列上使用bigIncrements而不是increments。因此,此包的默认设置是使用相同的约定来处理two_factor_auths表上的user_id列。如果这不是您想要的,您可以修改此包发布的迁移文件。

  1. 运行以下artisan命令以运行数据库迁移
php artisan migrate

这将向users表添加一个mobile列并创建一个two_factor_auths表。

  1. 将以下特质添加到您的User模型中
...
use MichaelDzjap\TwoFactorAuth\TwoFactorAuthenticable;

class User extends Authenticatable
{
    use Notifiable, TwoFactorAuthenticable;
...

可选地,您可能想将'mobile'添加到您的$fillable数组中。

登录流程更改

以下双因素认证路由将自动添加

$router->group([
    'middleware' => ['web', 'guest'],
    'namespace' => 'App\Http\Controllers\Auth',
], function () use ($router) {
    $router->get('/auth/token', 'TwoFactorAuthController@showTwoFactorForm')->name('auth.token');
    $router->post('/auth/token', 'TwoFactorAuthController@verifyToken');
});

第一个路由是用户启动双因素认证流程后将被重定向到的路由。第二个路由用于验证用户将要输入的双因素认证令牌。showTwoFactorForm控制器方法正是这样做的。然而,在某些情况下,您可能想有不同的响应。例如,您可能只想返回一个json响应而不是加载一个视图。在这种情况下,您可以在下面的TwoFactorAuthController中简单地覆盖showTwoFactorForm

  1. 将以下导入添加到LoginController
...
use MichaelDzjap\TwoFactorAuth\Contracts\TwoFactorProvider;

class LoginController extends Controller
{
...

并添加以下函数

/**
 * The user has been authenticated.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  mixed  $user
 * @return mixed
 */
protected function authenticated(Request $request, $user)
{
    if (resolve(TwoFactorProvider::class)->enabled($user)) {
        return self::startTwoFactorAuthProcess($request, $user);
    }

    return redirect()->intended($this->redirectPath());
}

/**
 * Log out the user and start the two factor authentication state.
 *
 * @param  \Illuminate\Http\Request $request
 * @param  \App\Models\User $user
 * @return \Illuminate\Http\Response
 */
private function startTwoFactorAuthProcess(Request $request, $user)
{
    // Logout user, but remember user id
    auth()->logout();
    $request->session()->put(
        'two-factor:auth', array_merge(['id' => $user->id], $request->only('email', 'remember'))
    );

    self::registerUserAndSendToken($user);

    return redirect()->route('auth.token');
}

最后

/**
 * Provider specific two-factor authentication logic. In the case of MessageBird
 * we just want to send an authentication token via SMS.
 *
 * @param  \App\Models\User $user
 * @return mixed
 */
private function registerUserAndSendToken(User $user)
{
    // Custom, provider dependend logic for sending an authentication token
    // to the user. In the case of MessageBird Verify this could simply be
    // resolve(TwoFactorProvider::class)->sendSMSToken($this->user)
    // Here we assume this function is called from a queue'd job
    dispatch(new SendSMSToken($user));
}

如果您不想在成功登录尝试后自动发送双因素认证令牌,可以忽略第三个函数。相反,您可能希望用户自己从表单中启动此过程。在这种情况下,您必须添加必要的路由和控制器方法来触发此函数。最好的地方是下面的TwoFactorAuthController

  1. app/Http/Controllers/Auth中添加一个名为TwoFactorAuthController,内容如下
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use MichaelDzjap\TwoFactorAuth\Http\Controllers\TwoFactorAuthenticatesUsers;

class TwoFactorAuthController extends Controller
{
    use TwoFactorAuthenticatesUsers;

    /**
     * The maximum number of attempts to allow.
     *
     * @var int
     */
    protected $maxAttempts = 5;

    /**
     * The number of minutes to throttle for.
     *
     * @var int
     */
    protected $decayMinutes = 1;

    /**
     * Where to redirect users after two-factor authentication passes.
     *
     * @var string
     */
    protected $redirectTo = RouteServiceProvider::HOME;
}
  1. 如果您想在由于令牌过期或限制触发时失败时向用户提供文本反馈,您可以将其添加到resources/views/auth/login.blade.php
...
<form class="form-horizontal" role="form" method="POST" action="{{ route('login') }}">
    @csrf

    {{-- Add this block to show an error message in case of an expired token or user lockout --}}
    @if ($errors->has('token'))
        <div class="alert alert-danger alert-dismissible fade show" role="alert">
            <strong>{{ $errors->first('token') }}</strong>
            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
        </div>
    @endif
...

处理失败的验证尝试

默认行为是在令牌验证失败时重定向到上一个视图并显示错误消息。然而,很可能存在您希望以不同方式处理失败令牌验证尝试的情况。例如,在MessageBird的情况下,令牌只能验证一次。在第一次失败尝试后,使用相同令牌的任何尝试都将始终抛出TokenAlreadyProcessedException,因此,重新重定向到/login路由从头开始整个身份验证过程或者重定向到可以请求新令牌的视图会更合理。

为了更改默认行为,您可以在您的TwoFactorAuthController上指定$redirectToAfterFailure属性或受保护的redirectToAfterFailure方法。如果这些中的任何一个存在(方法优先于属性),则默认行为将被绕过,并且用户将被重定向到指定的路由。简单示例,假设您希望在验证尝试失败后重定向到/login路由,则您可以将您的TwoFactorAuthController结构化为

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use MichaelDzjap\TwoFactorAuth\Http\Controllers\TwoFactorAuthenticatesUsers;

class TwoFactorAuthController extends Controller
{
    use TwoFactorAuthenticatesUsers;

    /**
     * Where to redirect users after two-factor authentication passes.
     *
     * @var string
     */
    protected $redirectTo = RouteServiceProvider::HOME;

    /**
     * Where to redirect users after two-factor authentication fails.
     *
     * @var string
     */
    protected $redirectToAfterFailure = '/login';
}

将用户重定向到生成新身份验证令牌的路由需要更多的工作,但确实可以通过这种方式实现。

使用自定义提供者

从v2.1.0版本发布以来,您可以使用自己的自定义提供者。为此,您的提供者需要实现MichaelDzjap\TwoFactorAuth\Contracts\TwoFactorProvider(如果需要通过短信发送身份验证令牌,则可能还需要实现MichaelDzjap\TwoFactorAuth\Contracts\SMSToken)。

  1. 假设您的自定义提供者名称为'dummy',您应从服务提供者(可能是\App\Providers\AppServiceProvider)注册它到TwoFactorAuthManager
resolve(\MichaelDzjap\TwoFactorAuth\TwoFactorAuthManager)->extend('dummy', function ($app) {
    return new DummyProvider;
});
  1. app/config/twofactor-auth.php中的'provider'数组中添加您自定义提供者的条目。
...
'dummy' => [

    'driver' => 'dummy',

],
...
  1. 最后,不要忘记更改您的.env中的提供者名称。
TWO_FACTOR_AUTH_DRIVER=dummy

错误和异常

遗憾的是,当令牌验证失败时,MessageBird的php api会抛出相当通用的异常。区分过期令牌和无效令牌的唯一方法是比较它们的错误消息,这显然不是一个非常稳健的机制。这是因为,在无效令牌的情况下,我们希望用户至少有几次(3次)机会重新输入令牌,然后再启动节流,而在过期令牌的情况下,我们只想立即将用户重定向到登录屏幕。

测试

包括单元测试和浏览器测试的示例项目可以在这里找到。