Laravel 的 Auth::attempt () 初探及修改 bcrypt 验证为 MD5

如果你在使用Laravel的话,用户的代码只需要一行代码就可以搞定

if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {
    // 验证成功的逻辑
}

但是,如果你想切换为自定义的加密验证方式,那么这篇文章可能会给你一些思路
比如,如果我想把密码的验证方式更换为MD5,我应该怎么做呢?
别急,先从laravel框架的验证流程开始
我们调用的Auth::attempt()在哪里实现的呢?
先从Auth找起
config/app.php

'aliases' => [

        'App' => Illuminate\Support\Facades\App::class,
        'Artisan' => Illuminate\Support\Facades\Artisan::class,
        'Auth' => Illuminate\Support\Facades\Auth::class,
                // ...
        ]

我们看到我们调用Auth其实是调用了

 Illuminate\Support\Facades\Auth::class

打开这个类文件

 class Auth extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'auth';
    }
    // ...
}

可以看到,Auth是通过Facade动态绑定的,绑定到哪里呢,进一步寻找我们发现
vendor/laravel/framework/src/Illuminate/AuthServiceProvider

 class AuthServiceProvider extends ServiceProvider
{
    /**
     * Register the authenticator services.
     *
     * @return void
     */
    protected function registerAuthenticator()
    {
        $this->app->singleton('auth', function ($app) {
            $app['auth.loaded'] = true;
            return new AuthManager($app);
        });

        $this->app->singleton('auth.driver', function ($app) {
            return $app['auth']->guard();
        });
    }
}

默认的Auth绑定了AuthManager,打开AuthManager文件

 <?php
namespace Illuminate\Auth;

use Closure;
use InvalidArgumentException;
use Illuminate\Contracts\Auth\Factory as FactoryContract;
class AuthManager implements FactoryContract
{
    use CreatesUserProviders;

    protected $app;

    protected $guards = [];

    public function guard($name = null)
    {
        $name = $name ?: $this->getDefaultDriver();

        return isset($this->guards[$name])
                    ? $this->guards[$name]
                    : $this->guards[$name] = $this->resolve($name);
    }

    public function getDefaultDriver()
    {
        return $this->app['config']['auth.defaults.guard'];
    }

    public function __call($method, $parameters)
    {

        return $this->guard()->{$method}(...$parameters);
    }
}

并没有找到attempt方法,不过有一个__call的魔术方法,那肯定是他里面没错了,为了快速找到他究竟是何方神圣,直接用

 dd(get_class($this->guard()));

真正的attempt究竟被谁调用了呢?
打印了SessionGuard,继续找下去

Illuminate\Auth\SessionGuard

打开该类,发现终于发现了我们寻找好久的attempt的实现

class SessionGuard implements StatefulGuard, SupportsBasicAuth
{
    use GuardHelpers, Macroable;
    public function attempt(array $credentials = [], $remember = false)
    {
        $this->fireAttemptEvent($credentials, $remember);

        $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
        if ($this->hasValidCredentials($user, $credentials)) {
            $this->login($user, $remember);

            return true;
        }
        $this->fireFailedEvent($user, $credentials);

        return false;
    }

这就是我们一直使用的attempt的实现,通过 $this->provider->retrieveByCredentials($credentials)获取用户信息,并验证,如果成功则登录,并返回true
所以我们真正做的密码验证肯定在retrieveByCredentials这个方法里面
Laravel 默认提供了 UserProviderEloquentUserProvider
打开改方法

class EloquentUserProvider implements UserProvider
{
    protected $hasher;

    protected $model;
    public function __construct(HasherContract $hasher, $model)
    {
        $this->model = $model;
        $this->hasher = $hasher;
    }
    public function validateCredentials(UserContract $user, array $credentials)
    {

        $plain = $credentials['password'];
        return $this->hasher->check($plain, $user->getAuthPassword());
    }
    public function setHasher(HasherContract $hasher)
    {
        $this->hasher = $hasher;

        return $this;
    }
}

所以这里的hasher就是系统默认的BcryptHasher了,我们修改他直接注入自己的haser
ok,了解思路了,开始搞它

1.编写自己的hasher

<?php

namespace App\Helpers\Hasher;

use Illuminate\Contracts\Hashing\Hasher;

class MD5Hasher implements Hasher
{
    public function check($value, $hashedValue, array $options = [])
    {

        return $this->make($value) === $hashedValue;
    }

    public function needsRehash($hashedValue, array $options = [])
    {
        return false;
    }

    public function make($value, array $options = [])
    {
        $value = env('SALT', '').$value;

        return md5($value);
    }

}

2.用自己的Hasher替换默认的Hasher

创建MD5HashServiceProvider

php artisan make:provider MD5HashServiceProvider

添加如下方法

<?php

namespace App\Providers;

use App\Helpers\Hasher\MD5Hasher;
use Illuminate\Support\ServiceProvider;

class MD5HashServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->app->singleton('hash', function () {
            return new MD5Hasher;
        });
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    public function provides()
    {
        return ['hash'];
    }
}

然后在config/app.phpproviders中,将

Illuminate\Hashing\HashServiceProvider::class,

替换为

\App\Providers\MD5HashServiceProvider::class,

OK,大功告成

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由 Summer 于 6年前 加精
王举
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 11
王举

@Hanccc 因为之前数据库用户的密码全部是MD5,这只是提供一种修改修改密码加密的思路,当然可以修改各种加密方式

6年前 评论
王举

@linzi007 谢谢,刚刚发现没有remember_token的时候会报错。但是个人不太建议重写EloquentUserProvider,
我看了下EloquentUserProvider中的源码如下

public function updateRememberToken(UserContract $user, $token)
    {
        $user->setRememberToken($token);

        $timestamps = $user->timestamps;

        $user->timestamps = false;

        $user->save();

        $user->timestamps = $timestamps;
    }

所以更新remember_token最终调用了$user->setRememberToken($token);,所以最省事的方法就是,
直接重写User模型的setRememberToken方法,就可以搞定啦

6年前 评论

只是,为什么要换成 MD5 呢

6年前 评论

如果原来的表没有 remember_token 字段,还要重写 provider 里的 update token 的方法

6年前 评论
jcc123

EloquentUserProvider 中的

public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials)) {
            return;
        }

        // First we will add each credential element to the query as a where clause.
        // Then we can execute the query and, if we found a user, return it in a
        // Eloquent User "model" that will be utilized by the Guard instances.
        $query = $this->createModel()->newQuery();

        foreach ($credentials as $key => $value) {
            if (! Str::contains($key, 'password')) {
                $query->where($key, $value);
            }
        }

        return $query->first();
    }

并没有调用

public function validateCredentials(UserContract $user, array $credentials)
    {
        $plain = $credentials['password'];

        return $this->hasher->check($plain, $user->getAuthPassword());
    }

它是怎么调用到这个方法的

6年前 评论
王举

@jc91715 注意SessionGuardattempt方法

6年前 评论

哈哈···不错···正好有相同的需求,谢啦

5年前 评论

如果是md5加密,而且还有salt 的情况呢??

5年前 评论

是不是要手动加active字段,@王举

5年前 评论

请问下楼主, $this->app->singleton('hash', function () {
return new MD5Hasher;
}); 在注册的时候 为啥不是注册 Hasher::class 而是hash

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!