首頁  >  文章  >  後端開發  >  透過修改Laravel Auth使用salt和password進行認證使用者詳解

透過修改Laravel Auth使用salt和password進行認證使用者詳解

巴扎黑
巴扎黑原創
2017-08-18 11:27:221659瀏覽

這篇文章主要為大家介紹了關於透過修改Laravel Auth使用salt和password進行認證用戶的相關資料,文中透過範例程式碼介紹的非常詳細,對大家的學習或工作具有一定的參考學習價值,需要的朋友們下面來一起看看吧。

前言

本文主要介紹給大家透過修改Laravel Auth用salt和password進行認證使用者的相關內容,分享出來供大家參考學習,下面話不多說了,來一起看看詳細的介紹:

Laraval自帶的用戶認證系統Auth非常強大易用,不過在Laravel的用戶認證系統中用戶註冊、登入、找回密碼這些模組中用到密碼加密和認證演算法時使用的都是bcrypt,而很多之前做的項目用戶表裡都是採用儲存salt + password加密字串的方式來記錄用戶的密碼的,這就給使用Laravel框架來重構之前的項目帶來了很大的阻力,不過最近自己通過在網上找資料、看社區論壇、看源碼等方式完成了對Laravel Auth的修改,在這裡分享出來希望能對其他人有幫助。 在開篇之前需要再說明下如果是新專案應用Laravel框架,那麼不需要對Auth進行任何修改,預設的bcrypt加密演算法是比salt + password更安全更有效率的加密演算法。

修改用戶註冊

首先,在laravel 啟用驗證是用的artisan指令


#
php artisan make:auth

執行完指令後在routes檔(位置:app/Http/routes.php)會多一條靜態方法呼叫

Route::auth();

這個Route是Laravel的一個Facade (位於Illuminate\Support\Facades\Route), 呼叫的auth方法定義在Illuminate\Routing\Router類別裡, 如下可以看到auth方法裡就是定義了一些Auth相關的路由規則

#
/**
 * Register the typical authentication routes for an application.
 *
 * @return void
 */
public function auth()
{
 // Authentication Routes...
 $this->get('login', 'Auth\AuthController@showLoginForm');
 $this->post('login', 'Auth\AuthController@login');
 $this->get('logout', 'Auth\AuthController@logout');

 // Registration Routes...
 $this->get('register', 'Auth\AuthController@showRegistrationForm');
 $this->post('register', 'Auth\AuthController@register');

 // Password Reset Routes...
 $this->get('password/reset/{token?}', 'Auth\PasswordController@showResetForm');
 $this->post('password/email', 'Auth\PasswordController@sendResetLinkEmail');
 $this->post('password/reset', 'Auth\PasswordController@reset');
}

透過路由規則可以看到註冊時請求的控制器方法是AuthController的register方法, 該方法定義在\Illuminate\Foundation\Auth\RegistersUsers這個traits裡,AuthController在類定義裡引入了這個traits.

/**
 * Handle a registration request for the application.
 *
 * @param \Illuminate\Http\Request $request
 * @return \Illuminate\Http\Response
 */
public function register(Request $request)
{
 $validator = $this->validator($request->all());

 if ($validator->fails()) {
 $this->throwValidationException(
  $request, $validator
 );
 }

 Auth::guard($this->getGuard())->login($this->create($request->all()));

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

在register方法里首先會對request裡的用戶輸入資料進行驗證,你只需要在AuthController的validator方法裡定義自己的每個輸入欄位的驗證規則就可以

<pre class="brush:php;toolbar:false">protected function validator(array $data) { return Validator::make($data, [ &amp;#39;name&amp;#39; =&gt; &amp;#39;required|max:255&amp;#39;, &amp;#39;email&amp;#39; =&gt; &amp;#39;required|email|max:255|unique:user&amp;#39;, &amp;#39;password&amp;#39; =&gt; &amp;#39;required|size:40|confirmed&amp;#39;, ]); }</pre>接著往下看驗證通過後,Laravel會掉用AuthController的create方法來產生新用戶,然後拿著新使用者的資料去登入

Auth::guard($this->getGuard())->login($this->create($request->all()));


所以我們要自訂使用者註冊時產生使用者密碼的加密方式只需要修改AuthController的create方法即可。

例如:

/**
 * Create a new user instance after a valid registration.
 *
 * @param array $data
 * @return User
 */
protected function create(array $data)
{
 $salt = Str::random(6);
 return User::create([
 &#39;nickname&#39; => $data[&#39;name&#39;],
 &#39;email&#39; => $data[&#39;email&#39;],
 &#39;password&#39; => sha1($salt . $data[&#39;password&#39;]),
 &#39;register_time&#39; => time(),
 &#39;register_ip&#39; => ip2long(request()->ip()),
 &#39;salt&#39; => $salt
 ]);
}
#修改使用者登入

# #修改登入前我們需要先透過路由規則看一下登入要求的具體控制器和方法,在上文提到的auth方法定義裡可以看到


 $this->get(&#39;login&#39;, &#39;Auth\AuthController@showLoginForm&#39;);
 $this->post(&#39;login&#39;, &#39;Auth\AuthController@login&#39;);
 $this->get(&#39;logout&#39;, &#39;Auth\AuthController@logout&#39;);
驗證登入的操作是在\App\Http\Controllers\Auth\AuthController類別的login方法裡。打開AuthController發現Auth相關的方法都是透過性狀(traits)引入到類別內的,在類別內use 要引入的traits,在編譯時PHP就會把traits裡的程式碼copy到類別中,這是PHP5.5引入的特性具體適用場景和用途這裡不細講。 所以AuthController@login
方法實際上是定義在

\Illuminate\Foundation\Auth\AuthenticatesUsers這個traits裡的


#

/**
 * Handle a login request to the application.
 *
 * @param \Illuminate\Http\Request $request
 * @return \Illuminate\Http\Response
 */
public function login(Request $request)
{
 $this->validateLogin($request);
 $throttles = $this->isUsingThrottlesLoginsTrait();

 if ($throttles && $lockedOut = $this->hasTooManyLoginAttempts($request)) {
 $this->fireLockoutEvent($request);

 return $this->sendLockoutResponse($request);
 }

 $credentials = $this->getCredentials($request);

 if (Auth::guard($this->getGuard())->attempt($credentials, $request->has(&#39;remember&#39;))) {
 return $this->handleUserWasAuthenticated($request, $throttles);
 }

 if ($throttles && ! $lockedOut) {
 $this->incrementLoginAttempts($request);
 }

 return $this->sendFailedLoginResponse($request);
}
登入驗證的主要操作是在Auth::guard($this->getGuard())->attempt($credentials, $request->has('remember'));這個方法呼叫中來進行的,Auth::guard($this->getGuard())

取得到的是\Illuminate\Auth\SessionGuard (具體如何取得的看Auth這個Facade \Illuminate\Auth\AuthManager裡的原始碼)

看看SessionGuard裡attempt 方法是如何實現的:


public function attempt(array $credentials = [], $remember = false, $login = true)
{
 $this->fireAttemptEvent($credentials, $remember, $login);

 $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);

 if ($this->hasValidCredentials($user, $credentials)) {
 if ($login) {
  $this->login($user, $remember);
 }

 return true;
 }

 if ($login) {
 $this->fireFailedEvent($user, $credentials);
 }

 return false;
}

/**
 * Determine if the user matches the credentials.
 *
 * @param mixed $user
 * @param array $credentials
 * @return bool
 */

protected function hasValidCredentials($user, $credentials)
{
 return ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
}

retrieveByCredentials是用傳遞進來的欄位從資料庫中取出使用者資料的, validateCredentials是用來驗證密碼是否正確的實際流程。

這裡要注意的是$this->provider

這個provider是一個實作了\Illuminate\Contracts\Auth\UserProvider類別的provider, 我們看到目錄Illuminate\Auth下面有兩個UserProvider的實現,分別為DatabaseUserProvider和EloquentUserProvider, 但是我們驗證密碼的時候是透過那個來驗證的呢,看一下auth的設定檔


###
&#39;providers&#39; => [
 &#39;users&#39; => [
 &#39;driver&#39; => &#39;eloquent&#39;,
 &#39;model&#39; => App\User::class, //这个是driver用的Model
 ],
],

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

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


/**
 * Retrieve a user by the given credentials.
 *
 * @param array $credentials
 * @return \Illuminate\Contracts\Auth\Authenticatable|null
 */
public function retrieveByCredentials(array $credentials)
{
 if (empty($credentials)) {
 return;
 }

 $query = $this->createModel()->newQuery();
 foreach ($credentials as $key => $value) {
 if (! Str::contains($key, &#39;password&#39;)) {
  $query->where($key, $value);
 }
 }
 return $query->first();
}

/**
 * Validate a user against the given credentials.
 *
 * @param \Illuminate\Contracts\Auth\Authenticatable $user
 * @param array $credentials
 * @return bool
 */
public function validateCredentials(UserContract $user, array $credentials)
{
 $plain = $credentials[&#39;password&#39;];

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

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

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

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


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


/**
 * 禁用Laravel自动管理timestamp列
 */
public $timestamps = false;

/**
 * 覆盖Laravel中默认的getAuthPassword方法, 返回用户的password和salt字段
 * @return type
 */
public function getAuthPassword()
{
 return [&#39;password&#39; => $this->attributes[&#39;password&#39;], &#39;salt&#39; => $this->attributes[&#39;salt&#39;]];
}

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

新建app/Foundation/Auth/AdminEloquentUserProvider.php


namespace App\Foundation\Auth;

use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Str;

class AdminEloquentUserProvider extends EloquentUserProvider
{

 /**
  * Validate a user against the given credentials.
  *
  * @param \Illuminate\Contracts\Auth\Authenticatable $user
  * @param array $credentials
  */
 public function validateCredentials(Authenticatable $user, array $credentials) {
  $plain = $credentials[&#39;password&#39;];
  $authPassword = $user->getAuthPassword();

  return sha1($authPassword[&#39;salt&#39;] . $plain) == $authPassword[&#39;password&#39;];
 }
}

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


&#39;providers&#39; => [
 &#39;users&#39; => [
  &#39;driver&#39; => &#39;admin-eloquent&#39;,
  &#39;model&#39; => App\User::class,
 ]
]

修改app/Provider/AuthServiceProvider.php


public function boot(GateContract $gate)
{
 $this->registerPolicies($gate);

 \Auth::provider(&#39;admin-eloquent&#39;, function ($app, $config) {
  return New \App\Foundation\Auth\AdminEloquentUserProvider($app[&#39;hash&#39;], $config[&#39;model&#39;]);
 });
}

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` (
 `email` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
 `token` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
 `created_at` timestamp NOT NULL,
 KEY `password_resets_email_index` (`email`),
 KEY `password_resets_token_index` (`token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

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


 $this->post(&#39;password/reset&#39;, &#39;Auth\PasswordController@reset&#39;);

可以看到对应的控制器方法是\App\Http\Controllers\Auth\PasswordController类的reset方法,这个方法实际是定义在\Illuminate\Foundation\Auth\ResetsPasswords 这个traits里,PasswordController引入了这个traits


/**
 * Reset the given user&#39;s password.
 *
 * @param \Illuminate\Http\Request $request
 * @return \Illuminate\Http\Response
 */
public function reset(Request $request)
{
 $this->validate(
  $request,
  $this->getResetValidationRules(),
  $this->getResetValidationMessages(),
  $this->getResetValidationCustomAttributes()
 );

 $credentials = $this->getResetCredentials($request);

 $broker = $this->getBroker();

 $response = Password::broker($broker)->reset($credentials, function ($user, $password) {
  $this->resetPassword($user, $password);
 });

 switch ($response) {
  case Password::PASSWORD_RESET:
   return $this->getResetSuccessResponse($response);
  default:
   return $this->getResetFailureResponse($request, $response);
 }
}

方法开头先通过validator对输入进行验证,接下来在程序里传递把新密码和一个闭包对象传递给Password::broker($broker)->reset();方法,这个方法定义在\Illuminate\Auth\Passwords\PasswordBroker类里.


/**
 * 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($credentials);

 if (! $user instanceof CanResetPasswordContract) {
  return $user;
 }

 $pass = $credentials[&#39;password&#39;];

 // 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[&#39;token&#39;]);

 return static::PASSWORD_RESET;
}

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


function ($user, $password) {
 $this->resetPassword($user, $password);
});

PasswrodController类resetPassword方法的定义


protected function resetPassword($user, $password)
{
 $user->forceFill([
  &#39;password&#39; => bcrypt($password),
  &#39;remember_token&#39; => Str::random(60),
 ])->save();

 Auth::guard($this->getGuard())->login($user);
}

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


/**
 * 覆盖ResetsPasswords traits里的resetPassword方法,改为用sha1(salt + password)的加密方式
 * Reset the given user&#39;s password.
 *
 * @param \Illuminate\Contracts\Auth\CanResetPassword $user
 * @param string $password
 * @return void
 */
protected function resetPassword($user, $password)
{
 $salt = Str::random(6);
 $user->forceFill([
  &#39;password&#39; => sha1($salt . $password),
  &#39;salt&#39; => $salt,
  &#39;remember_token&#39; => Str::random(60),
 ])->save();

 \Auth::guard($this->getGuard())->login($user);
}

结语

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

注:使用的Laravel版本为5.2

以上是透過修改Laravel Auth使用salt和password進行認證使用者詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn