澳门新浦京电子游戏通过修改Laravel Auth使用salt和password进行认证用户详解_php实例_脚本之家

昨天按照手册教程,动手写一个Auth扩展,按照包独立性的原则,我不希望将Auth::extend()这种方法写在
start.php
中,毫无疑问,我选择了在服务提供器register()方法中注册扩展驱动。然而,事与愿违……

前言

我们先来看看官方文档中对contracts的定义:

澳门新浦京电子游戏 1

本文主要给大家介绍了通过修改Laravel
Auth用salt和password进行认证用户的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍:

Laravel’s Contracts are a set of interfaces that define the core
services provided by the framework.意思是说Laravel的Contracts是一个由
框架提供 的定义了 核心服务接口 的集合。

发现问题

Laraval自带的用户认证系统Auth非常强大易用,不过在Laravel的用户认证系统中用户注册、登录、找回密码这些模块中用到密码加密和认证算法时使用的都是bcrypt,而很多之前做的项目用户表里都是采用存储salt
+
password加密字符串的方式来记录用户的密码的,这就给使用Laravel框架来重构之前的项目带来了很大的阻力,不过最近自己通过在网上找资料、看社区论坛、看源码等方式完成了对Laravel
Auth的修改,在这里分享出来希望能对其他人有所帮助。
开篇之前需要再说明下如果是新项目应用Laravel框架,那么不需要对Auth进行任何修改,默认的bcrypt加密算法是比salt

也就是说,每一个Contract都是一个接口,对应一个框架核心服务。

当我在 LoauthServiceProvider 中这样写的时候:

  • password更安全更高效的加密算法。

那它的意义何在?官网给出的解释也很简单:使用接口是为了 松耦合 和 简单 。

public function register()
{
    //
    Auth::extend('loauth',function($app){});
}

修改用户注册

先不讲大道理,先来点干货,看看怎么使用contract

报错

首先,在laravel 里启用验证是用的artisan命令

先浏览下contracts接口列表:

Call to undefined method IlluminateSupportFacadesAuth::extend()
php artisan make:auth

复制代码
代码如下:IlluminateContractsAuthGuardIlluminateContractsAuthPasswordBrokerIlluminateContractsBusDispatcherIlluminateContractsCacheRepositoryIlluminateContractsCacheFactoryIlluminateContractsConfigRepositoryIlluminateContractsContainerContainerIlluminateContractsCookieFactoryIlluminateContractsCookieQueueingFactoryIlluminateContractsEncryptionEncrypterIlluminateContractsRoutingRegistrar

寻找原因

执行完命令后在routes文件(位置:app/Http/routes.php)会多一条静态方法调用

…… 太多了,懒得继续贴了,官网手册里有。我们就拿
IlluminateContractsRoutingRegistrar
这个contract来演示一下吧。首先,打开
app/Providers/AppServiceProvider.php,注意register方法:

当时就纳闷了,找原因,怀疑是Auth没注册?检查发现注册了,因为在路由中可以使用;php
artisan clear-compiled
没用;百思不得其解,甚至怀疑是我不小心修改了核心类,还重新下载了一次laravel包,问题依旧。

Route::auth();

复制代码 代码如下:public function
register(){ $this->app->bind(
‘IlluminateContractsAuthRegistrar’, ‘AppServicesRegistrar’ );}

折腾了一晚上,最终我把目光锁定在 AuthServiceProvider 的 $defer 属性。

这个Route是Laravel的一个Facade
(位于IlluminateSupportFacadesRoute),
调用的auth方法定义在IlluminateRoutingRouter类里,
如下可以看到auth方法里就是定义了一些Auth相关的路由规则

$this->app 就是Application对象,也是容器对象,通过
$this->app->bind
方法我们绑定了一个实现IlluminateContractsAuthRegistrar接口的类AppServicesRegistrar。

根据手册以及注释,我们得知 $defer
属性是用来延迟加载该服务提供器,说直白点就是延迟执行 register()
方法,只需要配合provides()方法即可实现。举个例子:

/** * Register the typical authentication routes for an application. * * @return void */public function auth(){ // Authentication Routes... $this->get('login', 'AuthAuthController@showLoginForm'); $this->post('login', 'AuthAuthController@login'); $this->get('logout', 'AuthAuthController@logout'); // Registration Routes... $this->get('register', 'AuthAuthController@showRegistrationForm'); $this->post('register', 'AuthAuthController@register'); // Password Reset Routes... $this->get('password/reset/{token?}', 'AuthPasswordController@showResetForm'); $this->post('password/email', 'AuthPasswordController@sendResetLinkEmail'); $this->post('password/reset', 'AuthPasswordController@reset');}

注意,IlluminateContractsAuthRegistrar就是一个contract。AppServicesRegistrar
这个类文件在 app/Services/Registrar.php。

public function provides()
{
    return array('auth');
}

通过路由规则可以看到注册时请求的控制器方法是AuthController的register方法,
该方法定义在IlluminateFoundationAuthRegistersUsers这个traits里,AuthController在类定义里引入了这个traits.

接着我们看 AppHttpControllersAuthAuthController
这个控制器类,看到它有 __construct 构造函数:

这个是 AuthServiceProvider
里的方法,当框架初始化的时候,会依次加载服务提供器,如果发现这个服务提供器protected
$defer=true 那么就会调用它的 provides()
方法,其返回的数组包含需要延迟加载的服务名称,这样当我们在路由、控制器或者其他地方调用
Auth::METHOD() 的时候,才会去调用提供器的 register() 方法。

/** * Handle a registration request for the application. * * @param IlluminateHttpRequest $request * @return IlluminateHttpResponse */public function register{ $validator = $this->validator; if  { $this->throwValidationException( $request, $validator ); } Auth::guard->login($this->create; return redirect);}

复制代码 代码如下:public function
__construct(Guard $auth, Registrar $registrar){ $this->auth =
$auth; $this->registrar = $registrar;

确定症结

在register方法里首先会对request里的用户输入数据进行验证,你只需要在AuthController的validator方法里定义自己的每个输入字段的验证规则就可以

$this->middleware(‘guest’, [‘except’ => ‘getLogout’]);}

那么问题来了,既然是被动延迟加载,也就是说当我调用Auth类方法时应该会自动实例化Auth类啊,为什么我在LoauthServiceProvider中调用的时候却提示方法不存在,但是在路由中却可以呢。

protected function validator{ return Validator::make($data, [ 'name' => 'required|max:255', 'email' => 'required|email|max:255|unique:user', 'password' => 'required|size:40|confirmed', ]);}

它有两个参数,对应的类命名空间在脚本开头可以看到:

我猜测是因为优先级的问题,可能在框架注册
LoauthServiceProvider::register() 的时候,Auth
还没有标记为延迟加载,这就造成了一个先后问题,任何即时加载的服务提供器都无法在register方法中调用延迟加载的服务。

接着往下看验证通过后,Laravel会掉用AuthController的create方法来生成新用户,然后拿着新用户的数据去登录Auth::guard->login($this->create;

复制代码 代码如下:use
IlluminateContractsAuthGuard;use
IlluminateContractsAuthRegistrar;

经过研究,顺利在核心代码中找到证据
IlluminateFoundationProviderRepository

所以我们要自定义用户注册时生成用户密码的加密方式只需要修改AuthController的create方法即可。

这两个都是contract,但我们这里就拿 Registrar
说,我们注意到这里面只是通过参数类型指明了$registrar的接口类型,而实际调用的时候实际上是
AppServicesRegistrar
这个类,这就是依赖注入的特性了,Laravel会自动在容器中搜索实现了接口IlluminateContractsAuthRegistrar的类或对象,有的话就取出来作为实际参数传到构造函数里。

public function load(Application $app, array $providers)
{
    //...省略
    // We will go ahead and register all of the eagerly loaded providers with the
    // application so their services can be registered with the application as
    // a provided service. Then we will set the deferred service list on it.
    foreach ($manifest['eager'] as $provider)
    {
        $app->register($this->createProvider($app, $provider));
    }
    //延迟加载标记在即时加载服务之后
    $app->setDeferredServices($manifest['deferred']);
}
/** * Create a new user instance after a valid registration. * * @param array $data * @return User */protected function create{ $salt = Str::random; return User::create([ 'nickname' => $data['name'], 'email' => $data['email'], 'password' => sha1($salt . $data['password']), 'register_time' => time(), 'register_ip' => ip2long, 'salt' => $salt ]);}

整个使用流程其实就可以总结为两个步骤:

解决之道

修改用户登录

向容器中注册实现contract接口的对象。构造函数参数类型指定为contract接口类,框架会自动找到符合条件的对象。那么再来说说contract的好处。

虽然发现了问题所在,但并不代表问题就解决了,修改核心代码不是个明智的选择,所以只能在我们自己的包里想办法咯,一个解决方案如下:

修改登录前我们需要先通过路由规则看一下登录请求的具体控制器和方法,在上文提到的auth方法定义里可以看到

松耦合

public function register()
{
    //
    $authProvider = new IlluminateAuthAuthServiceProvider($this->app);
    $authProvider->register();
    Auth::extend('loauth',function($app){});
}
 $this->get('login', 'AuthAuthController@showLoginForm'); $this->post('login', 'AuthAuthController@login'); $this->get('logout', 'AuthAuthController@logout');

官网给了一个例子解释什么是紧耦合以及Contract接口为何能够松耦合。

既然auth还未注册,那么我们手动调用它的register方法帮它注册。

验证登录的操作是在AppHttpControllersAuthAuthController类的login方法里。打开AuthController发现Auth相关的方法都是通过性状引入到类内的,在类内use
要引入的traits,在编译时PHP就会把traits里的代码copy到类中,这是PHP5.5引入的特性具体适用场景和用途这里不细讲。
以AuthController@login方法实际是定义在IlluminateFoundationAuthAuthenticatesUsers这个traits里的

先来看看紧耦合的代码:

/** * Handle a login request to the application. * * @param IlluminateHttpRequest $request * @return IlluminateHttpResponse */public function login{ $this->validateLogin; $throttles = $this->isUsingThrottlesLoginsTrait(); if ($throttles && $lockedOut = $this->hasTooManyLoginAttempts { $this->fireLockoutEvent; return $this->sendLockoutResponse; } $credentials = $this->getCredentials; if (Auth::guard->attempt($credentials, $request->has { return $this->handleUserWasAuthenticated; } if ($throttles && ! $lockedOut) { $this->incrementLoginAttempts; } return $this->sendFailedLoginResponse;}

复制代码 代码如下:cache = $cache; } /**
* Retrieve an Order by ID. * * @param int $id * @return Order */
public function find { if ($this->cache->has { // } }}

登录验证的主要操作是在Auth::guard->attempt($credentials, $request->has;这个方法调用中来进行的,Auth::guard获取到的是IlluminateAuthSessionGuard
(具体如何获取的看Auth这个Facade IlluminateAuthAuthManager里的源码)

可以看到构造函数中注入了一个详细的缓存实现
SomePackageCacheMemcached
,如果换Redis作为缓存服务器或者更改了api方法,就需要修改,而如果项目很大,你不知道还有多少地方需要修改。

看一下SessionGuard里attempt 方法是如何实现的:

那么,Contract接口是如何解决这个问题的?请看代码:

public function attempt(array $credentials = [], $remember = false, $login = true){ $this->fireAttemptEvent($credentials, $remember, $login); $this->lastAttempted = $user = $this->provider->retrieveByCredentials; if ($this->hasValidCredentials { if  { $this->login; } return true; } if  { $this->fireFailedEvent; } return false;}/** * Determine if the user matches the credentials. * * @param mixed $user * @param array $credentials * @return bool */protected function hasValidCredentials{ return ! is_null && $this->provider->validateCredentials;}

复制代码 代码如下:cache = $cache; }}

retrieveByCredentials是用传递进来的字段从数据库中取出用户数据的,validateCredentials是用来验证密码是否正确的实际过程。

注意,缓存实现我们使用了一个接口,也就是contract,IlluminateContractsCacheRepository,因为它只是接口,不需要关心背后是memcache还是redis。

这里需要注意的是$this->provider这个provider是一个实现了IlluminateContractsAuthUserProvider类的provider,
我们看到目录IlluminateAuth下面有两个UserProvider的实现,分别为DatabaseUserProvider和EloquentUserProvider,
但是我们验证密码的时候是通过那个来验证的呢,看一下auth的配置文件

简单性

'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => AppUser::class, //这个是driver用的Model ],],

如果所有服务都使用接口定义,就可以很简单的决定一个服务需要的功能,更加容易维护和扩展,并且contract接口还能看作一个简洁的文档便于阅读。

这里配置的是driver => eloquent ,
那么就是通过EloquentUserProvider的retrieveByCredentials来验证的,
这个EloquentUserProvider 是在SessionGuard实例化时被注入进来的,
(具体是怎么通过读取auth配置文件,
实例化相应的provider注入到SessionGuard里的请查阅IlluminateAuthAuthManager
里createSessionDriver方法的源代码)

接下来我们继续查看EloquentUserProvider中retrieveByCredentials和validateCredentials方法的实现:

/** * Retrieve a user by the given credentials. * * @param array $credentials * @return IlluminateContractsAuthAuthenticatable|null */public function retrieveByCredentials{ if  { return; } $query = $this->createModel; foreach ($credentials as $key => $value) { if (! Str::contains { $query->where; } } return $query->first();}/** * Validate a user against the given credentials. * * @param IlluminateContractsAuthAuthenticatable $user * @param array $credentials * @return bool */public function validateCredentials(UserContract $user, array $credentials){ $plain = $credentials['password']; return $this->hasher->check($plain, $user->getAuthPassword;}

上面两个方法retrieveByCredentials用除了密码以外的字段从数据库用户表里取出用户记录,比如用email查询出用户记录,然后validateCredentials方法就是通过$this->haser->check来将输入的密码和哈希的密码进行比较来验证密码是否正确。

好了, 看到这里就很明显了,
我们需要改成自己的密码验证就是自己实现一下validateCredentials就可以了,
修改$this->hasher->check为我们自己的密码验证规则就可以了。

首先我们修改$user->getAuthPassword()把数据库中用户表的salt和password传递到validateCredentials中修改AppUser.php
添加如下代码

/** * The table associated to this model */protected $table = 'user';//用户表名不是laravel约定的这里要指定一下

/** * 禁用Laravel自动管理timestamp列 */public $timestamps = false;/** * 覆盖Laravel中默认的getAuthPassword方法, 返回用户的password和salt字段 * @return type */public function getAuthPassword(){ return ['password' => $this->attributes['password'], 'salt' => $this->attributes['salt']];}

然后我们在建立一个自己的UserProvider接口的实现,放到自定义的目录中:

新建app/Foundation/Auth/AdminEloquentUserProvider.php

namespace AppFoundationAuth;use IlluminateAuthEloquentUserProvider;use IlluminateContractsAuthAuthenticatable;use IlluminateSupportStr;class AdminEloquentUserProvider extends EloquentUserProvider{ /** * Validate a user against the given credentials. * * @param IlluminateContractsAuthAuthenticatable $user * @param array $credentials */ public function validateCredentials(Authenticatable $user, array $credentials) { $plain = $credentials['password']; $authPassword = $user->getAuthPassword(); return sha1($authPassword['salt'] . $plain) == $authPassword['password']; }}

最后我们修改auth配置文件让Laravel在做Auth验证时使用我们刚定义的Provider,修改config/auth.php:

'providers' => [ 'users' => [ 'driver' => 'admin-eloquent', 'model' => AppUser::class, ]]

修改app/Provider/AuthServiceProvider.php

public function boot{ $this->registerPolicies; Auth::provider('admin-eloquent', function  { return New AppFoundationAuthAdminEloquentUserProvider($app['hash'], $config['model']); });}

Auth::provider方法是用来注册Provider构造器的,这个构造器是一个Closure,provider方法的具体代码实现在AuthManager文件里

public function provider($name, Closure $callback){ $this->customProviderCreators[$name] = $callback; return $this;}

闭包返回了AdminEloquentUserProvider对象供Laravel
Auth使用,好了做完这些修改后Laravel的Auth在做用户登录验证的时候采用的就是自定义的salt

  • password的方式了。

修改重置密码

Laravel 的重置密码的工作流程是:

向需要重置密码的用户的邮箱发送一封带有重置密码链接的邮件,链接中会包含用户的email地址和token。
用户点击邮件中的链接在重置密码页面输入新的密码,Laravel通过验证email和token确认用户就是发起重置密码请求的用户后将新密码更新到用户在数据表的记录里。

第一步需要配置Laravel的email功能,此外还需要在数据库中创建一个新表password_resets来存储用户的email和对应的token

CREATE TABLE `password_resets`  COLLATE utf8_unicode_ci NOT NULL, `token` varchar COLLATE utf8_unicode_ci NOT NULL, `created_at` timestamp NOT NULL, KEY `password_resets_email_index` , KEY `password_resets_token_index`  ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

通过重置密码表单的提交地址可以看到,表单把新的密码用post提交给了/password/reset,我们先来看一下auth相关的路由,确定/password/reset对应的控制器方法。

 $this->post('password/reset', 'AuthPasswordController@reset');

可以看到对应的控制器方法是AppHttpControllersAuthPasswordController类的reset方法,这个方法实际是定义在IlluminateFoundationAuthResetsPasswords
这个traits里,PasswordController引入了这个traits

/** * Reset the given user's password. * * @param IlluminateHttpRequest $request * @return IlluminateHttpResponse */public function reset{ $this->validate( $request, $this->getResetValidationRules(), $this->getResetValidationMessages(), $this->getResetValidationCustomAttributes; $credentials = $this->getResetCredentials; $broker = $this->getBroker(); $response = Password::broker->reset($credentials, function  { $this->resetPassword; switch  { case Password::PASSWORD_RESET: return $this->getResetSuccessResponse; default: return $this->getResetFailureResponse; }}

方法开头先通过validator对输入进行验证,接下来在程序里传递把新密码和一个闭包对象传递给Password::broker;方法,这个方法定义在IlluminateAuthPasswordsPasswordBroker类里.

/** * Reset the password for the given token. * * @param array $credentials * @param Closure $callback * @return mixed */public function reset(array $credentials, Closure $callback){ // If the responses from the validate method is not a user instance, we will // assume that it is a redirect and simply return it from this method and // the user is properly redirected having an error message on the post. $user = $this->validateReset; if (! $user instanceof CanResetPasswordContract) { return $user; } $pass = $credentials['password']; // Once we have called this callback, we will remove this token row from the // table and return the response from this callback so the user gets sent // to the destination given by the developers from the callback return. call_user_func($callback, $user, $pass); $this->tokens->delete($credentials['token']); return static::PASSWORD_RESET;}

在PasswordBroker的reset方法里,程序会先对用户提交的数据做再一次的认证,然后把密码和用户实例传递给传递进来的闭包,在闭包调用里完成了将新密码更新到用户表的操作,
在闭包里程序调用了的PasswrodController类的resetPassword方法

function  { $this->resetPassword;

PasswrodController类resetPassword方法的定义

protected function resetPassword{ $user->forceFill([ 'password' => bcrypt, 'remember_token' => Str::random; Auth::guard->login;}

在这个方法里Laravel 用的是bcrypt 加密了密码, 那么要改成我们需要的salt +
password的方式,我们在PasswordController类里重写resetPassword方法覆盖掉traits里的该方法就可以了。

/** * 覆盖ResetsPasswords traits里的resetPassword方法,改为用sha1的加密方式 * Reset the given user's password. * * @param IlluminateContractsAuthCanResetPassword $user * @param string $password * @return void */protected function resetPassword{ $salt = Str::random; $user->forceFill([ 'password' => sha1, 'salt' => $salt, 'remember_token' => Str::random; Auth::guard->login;}

结语

到这里对Laravel
Auth的自定义就完成了,注册、登录和重置密码都改成了sha1的密码加密方式,
所有自定义代码都是通过定义Laravel相关类的子类和重写方法来完成没有修改Laravel的源码,这样既保持了良好的可扩展性也保证了项目能够自由迁移。

注:使用的Laravel版本为5.2

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

发表评论

电子邮件地址不会被公开。 必填项已用*标注