如何通过覆盖Auth结构在Laravel 8中允许使用主密码?

7

我有一个使用纯PHP编写的网站,现在正在学习Laravel,所以我重新制作这个网站来学习这个框架。我已经使用内置的Auth Fasade来进行身份验证。我想要理解内部发生了什么,所以我决定通过自定义来学习更多内容。现在我尝试创建一个主密码,它将允许直接访问每个帐户(就像过去一样)。

不幸的是,我找不到任何帮助来完成这件事。当我在寻找类似问题时,我只找到了一些解决方案,例如通过管理员登录,然后切换到另一个帐户旧版Laravel的解决方案等。

我开始自己研究Auth结构,但是我迷失了,甚至找不到检查密码的地方。我还在GitHub上找到了非常详细的解决方案,所以我尝试一步步跟随它,但是我无法制作自己的更简短实现。在我的旧网站中,我只需要一行代码就可以创建主密码,但是在Laravel中却有很多代码,我没有办法修改它。

例如,我尝试改变所有包含hasher->check部分的地方:

protected function validateCurrentPassword($attribute, $value, $parameters)
{
    $auth = $this->container->make('auth');
    $hasher = $this->container->make('hash');

    $guard = $auth->guard(Arr::first($parameters));

    if ($guard->guest()) {
        return false;
    }

    return $hasher->check($value, $guard->user()->getAuthPassword());
}

对于

return ($hasher->check($value, $guard->user()->getAuthPassword()) || $hasher->check($value, 'myHashedMasterPasswordString'));

ValidatesAttributesDatabaseUserProviderEloquentUserProviderDatabaseTokenRepository 中都有这个问题。但是,它没有起作用。我也一直在跟踪getAuthPassword()代码的所有实例,寻找更多线索。
我的其他解决方案是在某个地方放置像这样的代码:
if(Hash::check('myHashedMasterPasswordString',$given_password))
   Auth::login($user);

但我找不到一个合适的地方来处理它,无论是在中间件、提供者还是控制器中。

我已经学习了一些身份验证(Auth)功能,例如,我成功地更改了使用用户登录的电子邮件认证,但我想不出密码是如何工作的。有人能帮我解决我所遗漏的部分吗?如果需要更改哪些代码以及为什么要更改,我将不胜感激(如果这不是那么明显的话)。

我想一行一行地跟随代码执行,文件一步一步地看下去,也许我可以自己找到解决方法,但我感觉自己到处跳跃,没有任何思路,不知道这一切是如何相互关联的。


我无法帮助你解决具体的问题,但是一个用户扮演另一个用户(通常是相同或更低权限的用户)的一般概念被称为“模拟”,如果你搜索这个词,你会找到很多例子,比如 这里这里。作为系统管理员,主密码是令人害怕的,虽然模拟也是令人害怕的,但至少可以进行审计。 - Chris Haas
@ChrisHaas 感谢您指出这一点,我不知道它被称为这样,顺便说一下,即使它不能完全解决我的问题,我也很乐意检查它。在我的情况下,我想直接登录到任何帐户,而无需先登录到管理员帐户。也许我应该补充一下,我们正在使用局域网,仅供内部使用,所以这次我更重视快速访问而不是安全问题。 - Kida
密码将与“用户提供程序”中的哈希值进行检查,例如“EloquentUserProvider”,在“validateCredentials”函数中进行。如果您正在寻找发生这种情况的位置,则为“SessionGuard@attempt -> @hasValidCredentials -> UserProvider@validateCredentials”。 - lagbox
试试这个:https://github.com/404labfr/laravel-impersonate - Nouphal.M
@Nouphal.M 正如我在之前的评论中指出的那样,我希望能够直接登录到任何帐户而无需先登录管理帐户。所以简短的回答是:如果用户使用主密码登录,则即使与他自己的密码不匹配,也要登录他。 - Kida
显示剩余3条评论
4个回答

2
首先,在回答问题之前,我必须说我看了你的问题后面的评论,很惊讶你在EloquentUserProviderDatabaseUserProvider类中的validateCredentials()方法中返回true的测试失败了。
我尝试了一下,在Laravel 8中它按预期工作。你只需要一个现有用户(电子邮件),并且您将通过提交任何非空密码来通过登录。
你真正使用的是两个类中的哪一个(因为你不需要编辑两个类)?这取决于您在auth.php配置文件中的驱动程序配置。
'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

如您所想,您可以在validateCredentials()方法中添加一个“或”来进行验证,将$credentials['password']与您的自定义主密码进行比较。
话虽如此,确认这是您必须添加主密码验证的地方后,我认为实现您目标的最佳方式(至少是我推荐的方式)是跟踪类/方法,从官方文档开始,该文档建议您通过Auth门面执行登录。
use Illuminate\Support\Facades\Auth;

class YourController extends Controller
{
    public function authenticate(Request $request)
    {
        //
        if (Auth::attempt($credentials)) {
            //
        }
        //
    }
}

你可以首先创建自己的控制器(或修改现有的控制器),并创建自己的Auth类,继承自门面(使用__callStatic方法动态处理调用):
use YourNamespace\YourAuth;

class YourController extends Controller
{
    //
    public function authenticate(Request $request)
    {
        //
        if (YourAuth::attempt($credentials)) {
            //
        }
        //
    }
}

//
 * @method static \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard guard(string|null $name = null)
//

class YourAuth extends Illuminate\Support\Facades\Facade
{
// 
}

使用相同的逻辑,覆盖堆栈跟踪中的所有相关方法,直到使用validateCredentials()方法,最终还将在您自己的CustomEloquentUserProvider类中重写该方法,该类将从原始的EloquentUserProvider类扩展。
这样,您就可以实现您的目标,并保持整个过程的正确覆盖,能够更新laravel安装而不会丢失您的工作。最坏的情况是什么?如果任何一个重写方法在原始类中发生了极大的变化,您将不得不修复它们中的任何一个(这种情况的可能性非常低)。
提示
在进行完全覆盖时,您可能希望添加一些重要更改,例如避免接口,直接使用您真正需要的类和方法。例如:Illuminate/Auth/SessionGuard::validate
您还希望在.env文件中将主密码保存为环境变量。例如:
// .env
MASTER_PASSWORD=abcdefgh

然后使用env()助手调用它:

if ($credentials['password'] === env('MASTER_PASSWORD')) {
//
}

愉快的旅程!


1
感谢您参考评论并进行广泛的解释 :) - Kida

1

一个更完整的解决方案是定义一个自定义守卫并使用它,而不是尝试创建自己的自定义身份验证机制。

首先,在config/auth.php中定义一个新的守卫:

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

注意: 它使用与默认的web守卫完全相同的设置。

第二步,创建一个位于App\Guards\MasterPasswordGuard的新守卫:

<?php

namespace App\Guards;

use Illuminate\Auth\SessionGuard;
use Illuminate\Support\Facades\Auth;

class MasterPasswordGuard extends SessionGuard
{
    public function attempt(array $credentials = [], $remember = false): bool
    {
        if ($credentials['password'] === 'master pass') {
            return true;
        } else {
            return Auth::guard('web')->attempt($credentials, $remember);
        }
    }
}

注意:

  • 您可以将'master pass'替换为环境/配置变量或直接硬编码。在这种情况下,我仅检查特定密码。您可能还希望与电子邮件检查配对使用
  • 如果未匹配主密码,则会回退到检查数据库的默认保护

第三步,在AuthServiceProviderboot方法中注册此新保护程序:

Auth::extend('master', function ($app, $name, array $config) {
    return new MasterPasswordGuard(
        $name,
        Auth::createUserProvider($config['provider']),
        $app->make('session.store'),
        $app->request
    );
});

第四步,在您的控制器或任何您希望验证凭据的位置使用以下代码:

Auth::guard('master')->attempt([
    'email' => 'email',
    'password' => 'pass'
]);

示例

注册路由:

Route::get('test', [LoginController::class, 'login']);

创建你的控制器:
namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;

class LoginController
{
    public function login()
    {
        dd(
            Auth::guard('master')->attempt([
                'email' => 'demo@demo.com',
                'password' => 'master pass'
            ]),

            Auth::guard('master')->attempt([
                'email' => 'demo@demo.com',
                'password' => 'non master'
            ]),
        );
    }
}

如果您访问此端点,您将看到:

其中true表示使用了主密码,false表示尝试搜索用户。


最终想法

  • 从安全角度来看,您正在为自己打开另一个攻击向量,这对系统的安全和用户数据的隐私非常有害。最好重新考虑一下。
  • 验证凭据应该理想地与控制器分开,并移动到Request中。这将有助于保持代码库更加清洁和可维护。

1
最优雅的方式可能是,只允许在一个暂存网站上执行此操作,而不是在正式网站上执行。 - Martin Zeitler
感谢您的努力。我很欣赏您逐步指导的方式。由于我是 Laravel 的初学者,Anibal 的回答 对我帮助最大,因为他提供了广泛的解释,但是您的回答也让我对框架的内部结构有了更多的了解。 - Kida

0

不要试图自己编写,你可以使用一个库来完成这个任务:
laravel-impersonate(它已经经过了更好的测试)。这个库还带有Blade指令;只需确保正确配置,因为默认情况下任何人都可以模拟其他人。


甚至有(或曾经有)基本支持可用:Auth::loginAsId()


-1
这里是一个可能的解决方案。
使用主密码,您可以使用loginUsingId函数。
通过用户名搜索用户,然后检查密码是否与主密码匹配,如果匹配,则使用找到的用户ID登录。
public function loginUser($parameters)
{
    $myMasterHashPassword = "abcde";
    $username = $parameters->username;
    $password = $parameters->password;
    $user = User::where('username', $username)->first();
    if (!$user) {
        return response("Username not found", 404);
    }
    if (Hash::check($myMasterHashPassword, $password)) {
        Auth::loginUsingId($user->id);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接