Laravel 5.2 Auth 改用 salt+passwrod 加密驗證的實現

Ryan發表於2016-04-12

本文原連結來自我的部落格,地址:Laravel 5.2 Auth 認證解析以及改用 salt+passwrod 加密驗證

Larval 5.2的預設Auth登陸傳入郵件和使用者密碼到attempt 方法來認證,通過email的值獲取,如果使用者被找到,經雜湊運算後儲存在資料中的password將會和傳遞過來的經雜湊運算處理的passwrod值進行比較。如果兩個經雜湊運算的密碼相匹配那麼將會為這個使用者開啟一個認證Session。

但是往往我們一些系統中的密碼是通過salt+password的方式來做密碼認證的,或者一些老的系統是通過salt+passwrod來認證的,現在重構遷移到Laravel框架中,那麼密碼認證如何不用預設的passwrod的方式而用salt+password的方式認證?

要解決問題,我們最好還是先要弄明白根源,順藤摸瓜

首先看一下Laravel預設如何做密碼驗證的,看看Auth::guard($this->getGuard())->attempt($credentials)方法做了什麼:

Illuminate/Contracts/Auth/StatefulGuard.php

namespace Illuminate\Contracts\Auth;

interface StatefulGuard extends Guard
{
    /**
     * Attempt to authenticate a user using the given credentials.
     *
     * @param  array  $credentials
     * @param  bool   $remember
     * @param  bool   $login
     * @return bool
     */
    public function attempt(array $credentials = [], $remember = false, $login = true);
  ......

上面程式碼看到attemptStatefulGuard 介面中的方法,第一個引數為需要認證的欄位,第二個引數為是否記住登陸,第三個引數是否登陸,繼續往下看attempt 在SessionGuard中是如何實現的

illuminate/auth/SessionGuard.php

class SessionGuard implements StatefulGuard, SupportsBasicAuth
{
    use GuardHelpers;
    ......
    /**
     * Attempt to authenticate a user using the given credentials.
     *
     * @param  array  $credentials
     * @param  bool   $remember
     * @param  bool   $login
     * @return bool
     */
    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;
        }

        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);
    }
.......
}

看到通過 $this->provider->retrieveByCredentials($credentials);$this->provider->validateCredentials($user, $credentials);來實現驗證,retrieveByCredentials是用來驗證傳遞的欄位查詢使用者記錄是否存在,validateCredentials才是通過使用者記錄中密碼和傳入的密碼做驗證的實際過程。

這裡需要注意的是$this->provider,這個provider其實是實現了一個Illuminate\Contracts\Auth\UserProviderProvider,我們看到Illuminate/Contracts/Auth下面有兩個UserProvider的實現,分別為DatabaseUserProvider.phpEloquentUserProvider.php。但是我們驗證密碼的時候是通過哪個來驗證的是在怎麼決定的?

config/auth.php

 'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class, //這是User Model
        ],
    ],

這裡我配置了'driver' => 'eloquent',那麼就是通過EloquentUserProvider.php中的retrieveByCredentials來驗證的了,我們繼續看看它都幹了啥

illuminate/auth/EloquentUserProvider.php

class EloquentUserProvider implements UserProvider
{
......
 /**
     * Retrieve a user by the given credentials.
     *
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        // 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();
    }

   /**
     * 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['password'];

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

上面兩個方法 retrieveByCredentials用除了密碼以外的驗證欄位檢視記錄是否存在,比如用email來查詢使用者記錄是否存在, 然後 validateCredentials 方法就是通過 $this->hasher->check來將輸入的密碼和雜湊的密碼比較來驗證密碼是否正確,$plain 是提交過來的為加密密碼字串,$user->getAuthPassword()是資料庫記錄存放的加密密碼字串。

好了,看到這裡就很明顯了,我們需要改成我們自己的密碼驗證不就是自己實現一下validateCredentials方法就可以了嗎,改變 $this->hasher->check為我們自己的密碼驗證就可以了,開始搞吧!

  • 首先我們來實現$user->getAuthPassword();把資料庫中使用者表的saltpassword傳遞到validateCredentials中來:

修改 App\Models\User.php 新增如下程式碼

  public function getAuthPassword()
    {
        return ['password' => $this->attributes['password'], 'salt' => $this->attributes['salt']];
    }
  • 然後我們建立一個自己的UserProvider.php 的實現,你可以放到任何地方,我放到自定義目錄中:

新建 app/Foundation/Auth/RyanEloquentUserProvider.php

<?php namespace App\Foundation\Auth;

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

class RyanEloquentUserProvider extends EloquentUserProvider
{

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

我這裡通過$user->getAuthPassword();傳遞過來了使用者記錄的saltpassword,然後將認證提交的密碼$plainsalt進行加密,如果加密結果和使用者資料庫中記錄的密碼字串匹配那麼認證就通過了, 當然加密的演算法完全是自定義的。

  • 最後我們將User Providers換成我們自己的RyanEloquentUserProvider

修改 app/Providers/AuthServiceProvider.php

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

        \Auth::provider('ryan-eloquent', function ($app, $config) {
            return new RyanEloquentUserProvider($this->app['hash'], $config['model']);
        });
    }

修改 config/auth.php

 'providers' => [
        'users' => [
            'driver' => 'ryan-eloquent',
            'model' => App\Models\User::class,
        ],
    ],

好了,再試試可以用過salt+passwrod的方式密碼認證了!

轉載請註明: 轉載自Ryan是菜鳥 | LNMP技術棧筆記

如果覺得本篇文章對您十分有益,何不 打賞一下

謝謝打賞

相關文章