詳解如何修改Laravel Auth使用salt和password來認證用戶

前言

Laraval自帶的用戶認證系統Auth很是強大易用,不過在Laravel的用戶認證系統中用戶註冊、登陸、找回密碼這些模塊中用到密碼加密和認證算法時使用的都是bcrypt,而不少以前作的項目用戶表裏都是採用存儲salt + password加密字符串的方式來記錄用戶的密碼的,這就給使用Laravel框架來重構以前的項目帶來了很大的阻力,不過最近本身經過在網上找資料、看社區論壇、看源碼等方式完成了對Laravel Auth的修改,在這裏分享出來但願能對其餘人有所幫助。 開篇以前須要再說明下若是是新項目應用Laravel框架,那麼不須要對Auth進行任何修改,默認的bcrypt加密算法是比salt + password更安全更高效的加密算法。php

修改用戶註冊

首先,在laravel 裏啓用驗證是用的artisan命令laravel

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

經過路由規則能夠看到註冊時請求的控制器方法是AuthControllerregister方法, 該方法定義在\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裏的用戶輸入數據進行驗證,你只須要在AuthControllervalidator方法裏定義本身的每一個輸入字段的驗證規則就能夠閉包

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

接着往下看驗證經過後,Laravel會掉用AuthControllercreate方法來生成新用戶,而後拿着新用戶的數據去登陸Auth::guard($this->getGuard())->login($this->create($request->all()));app

因此咱們要自定義用戶註冊時生成用戶密碼的加密方式只須要修改AuthControllercreate方法便可。
好比:框架

/**
 * 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([
        'nickname' => $data['name'],
        'email' => $data['email'],
        'password' => sha1($salt . $data['password']),
        'register_time' => time(),
        'register_ip' => ip2long(request()->ip()),
        'salt' => $salt
    ]);
}

修改用戶登陸

修改登陸前咱們須要先經過路由規則看一下登陸請求的具體控制器和方法,在上文提到的auth方法定義裏能夠看到dom

$this->get('login', 'Auth\AuthController@showLoginForm');
    $this->post('login', 'Auth\AuthController@login');
    $this->get('logout', 'Auth\AuthController@logout');

驗證登陸的操做是在\App\Http\Controllers\Auth\AuthController類的login方法裏。打開AuthController發現Auth相關的方法都是經過性狀(traits)引入到類內的,在類內use 要引入的traits,在編譯時PHP就會把traits裏的代碼copy到類中,這是PHP5.5引入的特性具體適用場景和用途這裏不細講。 因此AuthController@login方法實際是定義在
\Illuminate\Foundation\Auth\AuthenticatesUsers這個traits裏的ide

/**
 * 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('remember'))) {
        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的實現,分別爲DatabaseUserProviderEloquentUserProvider, 可是咱們驗證密碼的時候是經過那個來驗證的呢,看一下auth的配置文件

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

這裏配置的是driver => eloquent, 那麼就是經過EloquentUserProviderretrieveByCredentials來驗證的, 這個EloquentUserProvider 是在SessionGuard實例化時被注入進來的, (具體是怎麼經過讀取auth配置文件, 實例化相應的provider注入到SessionGuard裏的請查閱\Illuminate\Auth\AuthManagercreateSessionDriver方法的源代碼)

接下來咱們繼續查看EloquentUserProviderretrieveByCredentialsvalidateCredentials方法的實現:

/**
 * 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, '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->haser->check來將輸入的密碼和哈希的密碼進行比較來驗證密碼是否正確。

好了, 看到這裏就很明顯了, 咱們須要改爲本身的密碼驗證就是本身實現一下validateCredentials就能夠了, 修改$this->hasher->check爲咱們本身的密碼驗證規則就能夠了。

首先咱們修改$user->getAuthPassword()把數據庫中用戶表的salt和password傳遞到validateCredentials
修改App\\User.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 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['password'];
        $authPassword = $user->getAuthPassword();

        return sha1($authPassword['salt'] . $plain) == $authPassword['password'];
    }
}

最後咱們修改auth配置文件讓Laravel在作Auth驗證時使用咱們剛定義的Provider,
修改config/auth.php:

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

修改app/Provider/AuthServiceProvider.php

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

    \Auth::provider('admin-eloquent', function ($app, $config) {
        return New \App\Foundation\Auth\AdminEloquentUserProvider($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 的重置密碼的工做流程是:

  1. 向須要重置密碼的用戶的郵箱發送一封帶有重置密碼連接的郵件,連接中會包含用戶的email地址和token。
  2. 用戶點擊郵件中的連接在重置密碼頁面輸入新的密碼,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('password/reset', 'Auth\PasswordController@reset’);

能夠看到對應的控制器方法是\App\Http\Controllers\Auth\PasswordController類的reset方法,這個方法實際是定義在\Illuminate\Foundation\Auth\ResetsPasswords 這個traits裏,PasswordController引入了這個traits

/**
 * Reset the given user'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['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 ($user, $password) {
    $this->resetPassword($user, $password);
});

PasswrodController類resetPassword方法的定義

protected function resetPassword($user, $password)
{
    $user->forceFill([
        'password' => bcrypt($password),
        'remember_token' => 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's password.
 *
 * @param  \Illuminate\Contracts\Auth\CanResetPassword  $user
 * @param  string  $password
 * @return void
 */
protected function resetPassword($user, $password)
{
    $salt = Str::random(6);
    $user->forceFill([
        'password' => sha1($salt . $password),
        'salt' => $salt,
        'remember_token' => Str::random(60),
    ])->save();

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

結語

到這裏對Laravel Auth的自定義就完成了,註冊、登陸和重置密碼都改爲了sha1(salt + password)的密碼加密方式, 全部自定義代碼都是經過定義Laravel相關類的子類和重寫方法來完成沒有修改Laravel的源碼,這樣既保持了良好的可擴展性也保證了項目可以自由遷移。

注:使用的Laravel版本爲5.2

相關文章
相關標籤/搜索