淺析 Laravel 自帶的使用者認證邏輯

ztlcoder發表於2019-12-07

一、前期準備

生成資料表

.env檔案中配置好資料庫,執行php artisan migrate,此時會在資料庫生成我們需要的幾張表

生成檔案

安裝laravelui

composer require laravel/ui

生成 登入/註冊 腳手架

php artisan ui vue --auth

此處可以使用vue或者react或者bootstrap,在執行該命令前可以觀察一下resources/views目錄和routes/web.php檔案內容。執行完這條命令,就會發現在根目錄下多了一個webpack.mix.js檔案,/routes/web.php檔案多了以下內容:

Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');

/resources/views目錄下多了auth資料夾、layouts資料夾和home.blade.php檔案。

此時,在瀏覽器中輸入yourdomain/home會發現自動跳轉到login頁面了,例如我本地的開發域名設定為laravel.do,開啟laravel.do/home就會跳轉到laravel.do/login,不過樣式...很醜!開啟除錯視窗,發現報錯找不到app.cssapp.js檔案,因為我們根本還沒有生成。不急,接下來先美化一下樣式。

利用Laravel Mix美化頁面

Laravel Mix是什麼就不解釋,不知道的同學可以看文件哦!使用Laravel Mix之前要保證已經安裝了nodejsnpm:

node -v
npm -v

如果能看到各自的版本號就代表已經安裝了,如果沒有,自行安裝。接著執行:

npm install

安裝完成後,即可執行Mix

// 執行 Mix 任務
npm run dev

// 執行所有的 Mix 任務並最小化輸出結果
npm run production

這裡我們就執行npm run production吧,發現public下多了css/app.cssjs/app.js還有mix-manifest.jsonmix-manifest.jsonmix生成的檔案清單,可是css/app.cssjs/app.js是從哪兒來的呢?上面提到根目錄多了一個檔案,對,就是webpack.mix.js,裡面有這麼一段程式碼:

mix.js('resources/js/app.js', 'public/js')
   .sass('resources/sass/app.scss', 'public/css');

這段程式碼的意思就是將resources/js/app.js編譯到public/js下並命名為app.js且將resources/sass/app.scss編譯到public/css下並命名為app.css,此時再開啟頁面,真好看!

二、分析認證

先從路由說起

可能有點囉嗦,其實就是為了說明如何得到那一堆路由的,不想看的可直接跳過。

前面說到routes/web.php檔案增加了兩行程式碼,第二行大家都知道,那麼第一行的Auth::routes();是什麼鬼?

不急,我們知道Auth使用了門臉方式,那麼一定有一個Auth.php檔案在名稱空間Illuminate\Support\Facades下。

名稱空間從vendor/laravel/framework/src開始,繼續往下找Illuminate/Support/Facades,果然找到了Auth.php檔案,內容不多,一個Auth類繼承了Facade,實現了兩個方法,第一個方法getFacadeAccessor定義了一個門臉訪問器,返回auth實現可以透過auth()輔助函式方式來訪問Auth,第二個方法routes就是用來生成認證路由的。

routes方法中只有一行程式碼:

static::$app->make('router')->auth($options);

static::$appIlluminate\Contracts\Foundation\Application的一個例項。

Illuminate\Contracts\Foundation\Application又繼承自Illuminate\Contracts\Container\Container,故而static::$app可以呼叫make方法。make方法利用PHP反射機制解析出一個類,這裡解析出的就是Illuminate\Routing\Router這個類,所以上面那一行程式碼最終執行的是Illuminate\Routing\Router類中的auth方法。

再來看auth方法。

這個方法就簡單多了,裡面定義了幾種路由,所以Auth::routes();最終得到的就是這樣的一堆路由:

Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
Route::post('login', 'Auth\LoginController@login');
Route::post('logout', 'Auth\LoginController@logout')->name('logout');
Route::get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
Route::post('register', 'Auth\RegisterController@register');
Route::get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
Route::get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
Route::post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');
Route::get('password/confirm', 'Auth\ConfirmPasswordController@showConfirmForm')->name('password.confirm');
Route::post('password/confirm', 'Auth\ConfirmPasswordController@confirm');
Route::get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
Route::get('email/verify/{id}/{hash}', 'Auth\VerificationController@verify')->name('verification.verify');
Route::post('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');

看著這一堆讓人頭大的路由,還是覺得Auth::routes()好啊!

再聊聊控制器

1、HomeController

HomeController的建構函式定義了訪問中介軟體auth

Kernel.php中的$routeMiddleware可以看到auth對映的是App\Http\Middleware\Authenticate類,這個類繼承了Illuminate\Auth\Middleware\Authenticate類,並定義了redirectTo方法。

開啟Illuminate\Auth\Middleware\Authenticate類,我們可以看到這個類很簡單,先是注入了Illuminate\Contracts\Auth\Factory的物件,然後在handle方法中呼叫了自身的authenticate方法,如果認證成功則返回使用者,認證失敗再呼叫unauthenticated,該方法會丟擲AuthenticationException異常,將redirectTo方法返回的結果作為第三個引數傳入其中。AuthenticationException會呼叫redirectTo方法進行跳轉。

關於為什麼會呼叫redirectTo進行跳轉,可以閱讀一下這個類Illuminate\Foundation\Exceptions\Handler

此時再看Illuminate\Auth\Middleware\Authenticate就清楚多了。

protected function redirectTo($request)
{
    if (! $request->expectsJson()) {
        return route('login');
    }
}

redirectTo方法覆蓋了父類方法,定義了未認證使用者如何跳轉。此處的意思是如果我們沒有期望返回值是json資料的話就返回命名為login的路由即:

Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');

2、LoginController

LoginController的建構函式定義了除了logout路由其它都訪問guest這個中介軟體。

Kernel.php中可以看到guest對映的是App\Http\Middleware\RedirectIfAuthenticated類,該類就做一件事,判斷使用者如果已經登入則跳轉到/home

LoginController還定義了一個保護屬性$redirectTo和使用了一個性狀AuthenticatesUsers

開啟Illuminate\Foundation\Auth\AuthenticatesUsers會發現,這個性狀就是一個使用者登入的完整的一套邏輯:顯示檢視、獲取資料、驗證資料、進行登入、登入成功響應、登入失敗響應、定義門衛、定義認證欄位等。該性狀內部還使用了RedirectsUsersThrottlesLogins性狀,前者的作用是重定向後者作用是限流。

LoginController中我們可以重定義showLoginForm方法來顯示其他檢視,可以重定義validateLogin方法來規定資料驗證規則,可以重定義username方法來指定要認證的欄位,可以重定義guard方法來重定義門衛等。

不過,目前資料庫中是空的,先點選頁面右上方的Register去註冊個使用者吧!點選之後,我們還是先想想Laravel做了什麼吧!

3、RegisterController

RegisterController的建構函式定義了訪問中介軟體guest,定義了跳轉地址$redirectTo = '/home',定義了一個資料驗證器validator和生成使用者記錄,同時也使用一個性狀RegistersUsers

照例開啟Illuminate\Foundation\Auth\RegistersUsers,再看這個性狀就容易多了,也是定義註冊使用者的一套邏輯:顯示檢視、定義門衛、進行註冊、已註冊處理。進行註冊的方法register內部分為4步:資料驗證、註冊成功事件監聽、使用者登入,註冊成功後頁面跳轉。

註冊成功事件監聽使用的是Illuminate\Auth\Events\Registered類,在App\Providers\EventServiceProvider服務提供者的監聽器中定義了事件對映:

protected $listen = [
    Registered::class => [
        SendEmailVerificationNotification::class,
    ],
];

Registered事件完成時,觸發SendEmailVerificationNotification,顧名思義就是傳送郵箱驗證通知。

接著我們在註冊頁面輸入好資訊,點選Register頁面直接跳轉到了/home,現在去我們剛剛註冊的郵箱裡看下,按道理來說是應該收到一封郵件驗證郵箱的郵件的,但現在什麼也沒有,這是怎麼回事呢?

開啟Illuminate\Auth\Listeners\SendEmailVerificationNotification看看就知道了。

public function handle(Registered $event)
{
    if ($event->user instanceof MustVerifyEmail && ! $event->user->hasVerifiedEmail()) {
        $event->user->sendEmailVerificationNotification();
    }
}

我們建立的使用者必須是介面Illuminate\Contracts\Auth\MustVerifyEmail的例項且郵箱還沒有被驗證才會傳送郵件。

開啟模型App\User我們會發現無論是App\User本身還是他繼承的Illuminate\Foundation\Auth\User都沒有實現介面Illuminate\Contracts\Auth\MustVerifyEmail,所以才沒有傳送驗證郵件,怎麼改造一下就可以送了,自己動動腦筋吧!

到這裡,我們就已經搞清楚使用者註冊登入的整個過程了,去登入頁輸入自己剛剛註冊的使用者,即可成功登入。接下來就該說說忘記密碼這一塊的邏輯了。

我們點選Forgot Your Password?發現跳轉到了laravel.do/password/reset,輸入之前註冊使用者時使用的郵箱,點選Send Password Reset Link,等了一會兒頁面直接報了一個錯:

Expected response code 250 but got code "530", with message "530 5.7.1 Authentication required
 "

這是怎麼回事呢?

4、ForgotPasswordController

password/reset訪問的是ForgotPasswordController控制器,可是ForgotPasswordController簡單的讓人頭皮發麻,就使用了一個性狀SendsPasswordResetEmails。不慌,開啟這個性狀你就會鎮定多了,依舊是Laravel實現的一套完整的忘記密碼的邏輯:顯示檢視、獲取郵箱、驗證郵箱、傳送郵件、傳送成功響應、傳送失敗響應等。

那麼為什麼會報錯呢?

因為還沒有配置郵箱!

.env中配置一下郵箱:

MAIL_DRIVER=smtp
MAIL_HOST=smtp.163.com
MAIL_PORT=25
MAIL_USERNAME=******
MAIL_PASSWORD=******
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=******@163.com
MAIL_FROM_NAME=Laravel社群

MAIL_DRIVER用於配置預設的郵件傳送驅動,此處的最佳選擇是SMTP,其它的要麼收費要麼不能使用。

MAIL_HOST是郵箱所在主機,對應值是smtp.163.com

MAIL_PORT用於配置郵箱傳送服務埠號,比如一般預設值是25,但如果MAIL_ENCRYPTION的值是ssl,該值為465。

MAIL_USERNAME表示郵箱賬號,比如******@163.com

MAIL_PASSWORD表示上述郵箱登入對應登入密碼。注意使用的是客戶端授權密碼

MAIL_ENCRYPTION表示加密型別,可以設定為null表示不使用任何加密,也可以設定為tlsssl

MAIL_FROM_ADDRESS表示傳送郵件使用的郵箱。

MAIL_FROM_NAME表示傳送郵件使用的名稱。

現在再去忘記密碼頁面輸入郵箱點選傳送就不會報錯了。進入自己的郵箱也會收到一封Laravel內建的漂亮的郵件,開啟郵件,點選Reset Password,有可能出現一個不是我們預期的頁面,原因也很簡單,因為傳送郵件的時候Laravel提取了域名拼接成了一個完整的URL,而域名的配置在app.php中:

'url' => env('APP_URL', 'http://localhost')

此處使用了env輔助函式從.env檔案中讀取APP_URL配置,如果沒有則使用預設值http://localhost。那麼我們去.env中把APP_URL換成http://laravel.do(這個是我本地設定的域名)即可。再重新傳送一封郵件,點選Reset Password就能進入我們預期的頁面了。

5、ResetPasswordController

ResetPasswordController中還是使用了性狀ResetsPasswords來負責完成重置密碼的邏輯。Illuminate\Foundation\Auth\ResetsPasswords的實現邏輯也很簡單,這裡就不做詳細的介紹了,大家可以自行理解。

三、一點想法

Laravel使用者認證的淺析到這裡就結束了,我們可以在控制器中覆蓋性狀中的方法來實現定製化開發,比如使用新的試圖,使用新的認證規則等,而這一切的實現也僅僅重新定義一個方法這麼簡單,這就是使用Laravel的魔力。

在沒有接觸Laravel之前根本沒用過trait官方手冊也看明白這是個啥,但就是不知道真實的使用場景,是Laravel讓我知道它的使用場景。而Laravel帶給我的也遠不止這些,自從半年前接觸到Laravel就深深地喜歡上了這款框架,喜歡它並不僅僅因為它很優美,更多的是它的設計思想和高效的開發,高效的開發帶來的好處自不必說,設計思想又可以讓我們學習到很多優秀的思想或者說實現方式,於我們能力的提升是有莫大的好處的,至於執行效率,我先是贊同這句話——程式是寫給人看的,機器只是恰巧能夠執行。隨著硬體和PHP自身的發展還有這麼多的最佳化手段,我想效能不應該是我們開發者首要考慮的因素

這一次也透過認真閱讀原始碼,發現閱讀原始碼不但沒那麼可怕,反而會讓我從中不斷找到樂趣,從猜想到猜想被驗證,每次都是一種肯定和成長。個人感覺從一個小功能入手閱讀原始碼可能會比較容易一點。

這是我第一次分享技術的文章,寫了整整半天,現在才體會到分享技術的不易,給樂於分享的技術者們點贊並向他們學習,文章之中如果有不對或不足之處還請大佬們指正,以免誤導他人!

本作品採用《CC 協議》,轉載必須註明作者和本文連結
不二

相關文章