## **簡介**
Laravel 框架開箱為我們提供了一些用戶認證需要的腳手架代碼,包括數據庫遷移文件、用戶模型、用戶認證中間件和控制控制器等。我們先來簡單介紹下它們。
- 數據庫遷移:新安裝的 Laravel 應用都包含下面兩個遷移文件,分別用于創建用戶表和密碼重置表;

- User模型類:Laravel 框架還在`app`目錄下為我們提供了與用戶表相對應的模型類`User`,在基于 Eloquent 模型驅動的認證提供者中,我們通過該模型類實現登錄認證,你可以在配置文件`config/auth.php`中查看相應的配置:
~~~
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
~~~
如果你不是通過`User`模型類進行認證,可以在這里修改對應的`model`配置項。如果你不想通過 Eloquent 模型驅動,而是基于原生的數據庫查詢,可以注釋掉`eloquent`對應的`users`配置,啟用下面這個`database`對應的`users`配置,這樣的話就是直接去查詢`users`表,而不是通過模型類進行認證了。
回到我們的默認配置,我們看下`User`模型類代碼:
~~~
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
}
~~~
如果某個模型類需要用于認證,必須繼承自`Illuminate\Foundation\Auth\User`基類,否則會報錯。然后我們在這個模型類中使用了`Notifiable`Trait,里面提供了用戶發送通知的相關方法。我們在白名單`$fillable`中配置了三個字段,這三個字段會在登錄和注冊時用到。最后,我們還配置了`$hidden`屬性,在返回查詢結果的時候將敏感信息過濾掉,避免安全隱患。
- 認證中間件:Laravel 框架內置了幾個認證中間件,用于在需要認證的路由中拒絕未認證用戶發起的請求,或者將已登錄用戶重定向到認證頁面,打開`app/Http/Kernel.php`,可以在`$routeMiddleware`看到對應的路由中間件:

我們平時主要用到的是`auth`中間件和`guest`中間件,`auth.basic`用于基于 HTTP 的簡單認證,很少用到,`throttle`中間件會在用戶多次登錄失敗時使用,單位登錄失敗超過指定次數不允許繼續發起登錄請求,提高系統安全性。
`auth`中間件是`\App\Http\Middleware\Authenticate::class`的別名,`Authenticate`主要用于將未登錄用戶重定向到登錄頁面。
`guest`中間件是`\App\Http\Middleware\RedirectIfAuthenticated::class`的別名,`RedirectIfAuthenticated`主要用于將已登錄用戶重定向到認證后頁面,未登錄則繼續原來的請求。
- 認證控制器:Laravel 還為我們開箱提供了注冊、登錄、重置密碼、郵箱驗證、忘記密碼對應的控制器:

其中`ForgotPasswordController`用于忘記密碼后通過填寫注冊郵箱發送重置密碼鏈接,對應邏輯位于`Illuminate\Foundation\Auth\SendsPasswordResetEmails`Trait 中;`LoginController`用于用戶登錄和退出,對應邏輯位于`Illuminate\Foundation\Auth\AuthenticatesUsers`Trait 中;`RegisterController`用于新用戶注冊,對應邏輯位于`Illuminate\Foundation\Auth\RegistersUsers`Trait 中;`ResetPasswordController`用于重置密碼,對應邏輯位于`Illuminate\Foundation\Auth\ResetsPasswords`Trait 中。
上述控制器的構造函數中都應用了`guest`中間件(退出功能除外),表示這些控制器提供的方法都是給未登錄用戶使用的。
`VerificationController`是 Laravel 5.7 新提供的,用于新注冊用戶郵箱驗證,對應邏輯位于`Illuminate\Foundation\Auth\VerifiesEmails`Trait 中。
## **通過Artisan命令快速實現注冊登錄**
我們可以自己編寫用戶認證路由、視圖然后對接到這些控制器,提供用戶認證功能,不過,Laravel 開箱為我們提供了 Artisan 命令`make:auth`,運行該命令可以幫助我們在最短時間內完成認證路由注冊和認證視圖(兼容 Bootstrap 4)發布,何樂而不為?
我們在系統根目錄下運行如下命令激活「休眠」的用戶認證功能:
~~~
php artisan make:auth # 注冊認證路由、發布認證視圖
php artisan migrate # 創建認證相關數據表,如果之前已經運行過,可以跳過
npm run dev # 編譯前端資源,如果之前已經運行過,可以跳過
~~~
`make:auth`命令會在`routes/web.php`中注冊以下路由:
~~~
Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');
~~~
`home`路由是用戶認證成功后默認跳轉路由,此外,該命令還會在`resources/views`下發布以下登錄認證相關的視圖文件:
* `resources/views/home.blade.php`
* `resources/views/layouts/app.blade.php`
* `resources/views/auth/login.blade.php`
* `resources/views/auth/register.blade.php`
* `resources/views/auth/verify.blade.php`
* `resources/views/auth/passwords/email.blade.php`
* `resources/views/auth/passwords/reset.blade.php`
這樣,不需要編寫任何代碼,只需要簡單運行幾個命令,就可以在 Laravel 應用中實現登錄認證功能了。

## **獲取登錄用戶信息**
用戶登錄成功后,就可以獲取登錄用戶信息了,我們可以通過多種方式獲取用戶信息。
### **通過Auth門面**
我們可以在控制器中通過`Auth`門面快速獲取當前登錄用戶信息:
~~~
$user = Auth::user(); // 獲取當前登錄用戶的完整信息
$userId = Auth::id(); // 獲取當前登錄用戶 ID
~~~
上述返回的`$user`數據是一個`User`模型實例,可以通過它獲取用戶所有信息。此外,你還可以通過`Auth`門面提供的其他方法快速進行一些常見判斷,比如判斷用戶是否已經登錄,可以通過`Auth::check()`方法,如果已登錄,該方法返回`true`,否則返回`false`。相對的,還有`Auth::guest()`方法判斷用戶是否未登錄,邏輯與`Auth::check()`方法剛好相反。
### **Blade指令**
我們還可以在 Blade 視圖上使用上述門面方法進行流程判斷,此外,Blade 模板引擎還為我們提供了對應的快捷指令:
~~~
@auth
// 用戶已登錄...
@endauth
@guest
// 用戶未登錄...
@endguest
~~~
### **通過Request實例**
除了`Auth`門面外,我們還可以在控制器方法中通過`Request`請求對象實例獲取登錄用戶信息:
~~~
public function update(Request $request)
{
$user = $request->user(); # 獲取當前登錄用戶實例
}
~~~
獲取到的`$user`數據和通過`Auth::user()`返回結果完全一致。
> 注:盡量不要在控制器和視圖以外的地方使用`Auth`門面獲取用戶信息,在其他地方獲取可以通過數據傳遞的方式,因為服務類或模型類的應用場景不一定是 Web 層,有可能出現獲取不到 Session 而導致獲取數據為空的情況。
## **登錄失敗次數限制**
對于用戶登錄功能而言,框架底層會校驗登錄失敗次數,超過指定閾值會報錯。默認閾值是 1 分鐘內嘗試 5 次,超過這個次數就會提示失敗次數過多,過段時間再來嘗試。對于未登錄用戶而言,這個限制維度是基于 IP 的。對應的實現細節位于`Illuminate\Foundation\Auth\ThrottlesLogins`中。
如果你想要修改這個閾值,可以在`LoginController`控制器中通過設置如下屬性來實現,比如設置半小時內只能嘗試3次:
~~~
// 單位時間內最大登錄嘗試次數
protected $maxAttempts = 3;
// 單位時間值
protected $decayMinutes = 30;
~~~
## **支持用戶名/郵箱登錄**
Laravel 支持在用戶名和郵箱之間切換登錄,默認是通過注冊郵箱登錄的,如果你想要調整為通過用戶名登錄,很簡單,在`LoginController`控制器中定義一個`username()`方法重寫`AuthenticatesUsers`Trait 中的同名方法即可,Laravel 底層通過該方法定義登錄字段名,而不是寫死的,我們在該方法中返回登錄字段名:
~~~
public function username()
{
return 'name';
}
//記得把前端視圖對應的登錄表單字段調整為`name`,這樣就可以基于用戶名進行登錄了
~~~
實際開發中,有時候我們的登錄字段可能需要同時支持用戶名/注冊郵箱/手機號登錄【前提是這三個字段都是唯一的】,即用戶既可以在登錄框中輸入郵箱,也可以輸入用戶名,這個時候,Laravel 框架底層自帶的 UserProvider 已經滿足不了這個需求了,我們需要擴展默認的`EloquentUserProvider`來實現這個功能。
為此,我們需要創建一個繼承自底層`EloquentUserProvider`的子類,并將其存放到`app/Extensions`目錄下,重寫父類用戶記錄獲取方法`retrieveByCredentials`如下:
~~~
<?php
namespace App\Extentions;
use Illuminate\Support\Str;
use Illuminate\Auth\EloquentUserProvider as BaseUserProvider;
class EloquentUserProvider extends BaseUserProvider
{
/**
* Retrieve a user by the given credentials.
*
* @param array $credentials
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials) ||
(count($credentials) === 1 &&
array_key_exists('password', $credentials))) {
return;
}
// 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();
// 用于標識是否是第一個登錄字段,如果包含多個登錄字段,使用 OR 查詢
$flag = false;
foreach ($credentials as $key => $value) {
if (Str::contains($key, 'password')) {
continue;
}
if ($flag) {
$query->orWhere($key, $value);
} else {
$query->where($key, $value);
$flag = true;
}
}
return $query->first();
}
}
~~~
核心是獲取用戶記錄那段邏輯,我們支持用戶登錄憑證數組`$credentials`除密碼字段外傳入一個或多個登錄字段,多個登錄字段使用 OR 查詢。
然后我們需要到`app/Providers/AuthServiceProvider.php`的`boot`方法中使用自定義的 UserProvider 覆蓋系統自帶的`EloquentUserProvider`:
~~~
// 通過自定義的 EloquentUserProvider 覆蓋系統默認的
Auth::provider('eloquent', function ($app, $config) {
return new EloquentUserProvider($app->make('hash'), $config['model']);
});
~~~
接下來,我們到控制器中修改傳入字段邏輯以便在業務層支持傳入不同字段。打開`app/Http/Controllers/Auth/LoginController.php`控制器文件,為控制器設置一個新的屬性用于包含系統支持的登錄字段:
~~~
// 支持的登錄字段
protected $supportFields = ['name', 'email'];
~~~
然后重寫`AuthenticatesUsers`中的`credentials`方法用于傳入系統支持的所有登錄字段,并將其值都設置為用戶在登錄表單輸入框中輸入的值:
~~~
// 將支持的登錄字段都傳遞到 UserProvider 進行查詢
public function credentials(Request $request)
{
$credentials = $request->only($this->username(), 'password');
foreach ($this->supportFields as $field) {
if (empty($credentials[$field])) {
$credentials[$field] = $credentials[$this->username()];
}
}
return $credentials;
}
~~~
最后去掉登錄視圖`login.blade.php`對郵箱字段的驗證,以支持不同的字段輸入:
~~~
<div class="form-group row">
<label for="email" class="col-sm-4 col-form-label text-md-right">{{ __('郵箱') . '/' . __('用戶名') }}</label>
<div class="col-md-6">
<input id="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required autofocus>
@if ($errors->has('email'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</div>
</div>
~~~
這樣多字段登錄功能就完成了,如果你還有其他支持登錄的字段,比如用戶昵稱、手機號,都可以實現,只需在控制器的`$supportFields`屬性中添加對應的字段就可以了。