# 用戶認證
> {tip}**想要快點開始?**只需在新的 Laravel 應用上運行`php artisan make:auth`和`php artisan migrate`命令。 然后,可以用瀏覽器訪問`http://your-app.test/register`或者你在程序中定義的其他 URL 。 這兩個命令就可以構建好整個認證系統!
Laravel 中實現用戶認證非常簡單。事實上,幾乎所有的東西都已經為你配置好了。 其配置文件位于`config/auth.php`, 其中包含了用于調整認證服務行為的注釋清晰的選項配置。
其核心是由 Laravel 的認證組件的「看守器」和「提供器」組成。 看守器定義了該如何認證每個請求中的用戶。例如, Laravel 自帶的`session`看守器會使用 session 存儲 和 cookies來維護狀態。
提供器中定義了該如何從持久化的存儲數據中檢索用戶。 Laravel 自帶支持使用 Eloquent 和 數據庫查詢構造器來檢索用戶。當然,你可以根據需要自定義其他提供器。
不過不用擔心,對于大多數應用而言,可能永遠都不需要修改默認身份認證配置。
### 數據庫注意事項
默認情況下, Laravel 在`app`目錄中包含了一個[Eloquent 模型](https://laravel-china.org/docs/laravel/5.7/eloquent)`App\User`。這個模型和默認的 Eloquent 認證驅動一起使用。如果你的應用不使用 Eloquent , 也可以使用 Laravel 查詢構造器的`database`認證驅動。
為`App\User`模型創建數據庫表結構時,確保密碼字段至少長度為 60 個字符以及默認字符串長度為 255 個字符。
此外,你要驗證的用戶(或等效的)表要包含一個空的、長度為 100 的字符串`remember_token`。這個字段將用于存儲當用戶登錄應用并勾選「記住我」時的令牌。
## 快速認證
Laravel 自帶幾個預構建的認證控制器,它們被放置在`App\Http\Controllers\Auth`命名空間內。`RegisterController`處理新用戶注冊,`LoginController`處理用戶認證,`ForgotPasswordController`處理用于重置密碼的郵件鏈接,而`ResetPasswordController`包含重置密碼的邏輯。這些控制器都使用 trait 來引入所必要的方法。對于大多數應用而言,你根本不需要修改這些控制器。
### 路由
Laravel 提供了一個簡單的命令來快速生成身份驗證所需的路由和視圖:
~~~php
php artisan make:auth
~~~
該命令最好在新的應用下使用,它會生成布局、注冊和登錄視圖以及所有的認證接口的路由。同時它還會生成`HomeController`來處理應用的登錄請求。
### 視圖
`php artisan make:auth`命令會在`resources/views/auth`目錄創建所有認證需要的視圖。
同時,`make:auth`命令還創建了`resources/views/layouts`目錄,該目錄包含了應用的基本布局視圖。所有這些視圖都是用 Bootstrap CSS 框架,你也可以根據需要對其自定義。
### 認證
現在,已經為認證的控制器設置好了路由和視圖,你可以為應用注冊和認證新用戶了!你現在可以在瀏覽器中訪問你的應用了,因為控制器已經默認包含了驗證用戶是否存在和保存用戶到數據庫中的認證邏輯(通過 traits實現的)。
#### 自定義路徑
當一個用戶成功通過身份認證后,將會重定向到`/home`的 URI。你可以通過在`LoginController`,`RegisterController`,和`ResetPasswordController`中設置`redirectTo`屬性來自定義重定向的路徑:
~~~php
protected $redirectTo = '/';
~~~
接下來,修改`RedirectIfAuthenticated`中間件的`handle`方法以便在重定向用戶時重定向到新的 URI。
如果重定向路徑需要自定義生成邏輯,你可以定義`redirectTo`方法替代`redirectTo`屬性:
~~~php
protected function redirectTo()
{
return '/path';
}
~~~
> {提示}`redirectTo`方法優先于`redirectTo`屬性。
#### 自定義用戶名
Laravel 默認使用`email`字段來認證。如果你想使用其他的字段,可以在`LoginController`控制器里面定義一個`username`方法:
~~~php
public function username()
{
return 'username';
}
~~~
#### 自定義看守器
你還可以自定義用戶認證和注冊的 「看守器」。要實現這一功能,需要在`LoginController`,`RegisterController`,和`ResetPasswordController`中定義`guard`方法。該方法需要返回一個看守器實例:
~~~php
use Illuminate\Support\Facades\Auth;
protected function guard()
{
return Auth::guard('guard-name');
}
~~~
#### 自定義驗證 / 存儲
為了修改新用戶在注冊時所需要填寫的表單字段,或者自定義如何將新用戶存儲到數據庫中,你可以修改`RegisterController`類。該類負責驗證和創建新用戶。`RegisterController`類的`validator`方法包含了驗證新用戶的規則,你可以按照需要自定義該方法。`RegisterController`的`create`方法負責使用[Eloquent ORM](https://laravel-china.org/docs/laravel/5.7/eloquent)在數據庫中創建新的`App\User`記錄。你可以根據數據庫的需要自定義該方法。
### 檢索認證用戶
你可以通過`Auth`facade 來訪問已認證的用戶:
~~~php
use Illuminate\Support\Facades\Auth;
// 獲取當前通過認證的用戶...
$user = Auth::user();
// 獲取當前通過認證的用戶 ID...
$id = Auth::id();
~~~
或者,你可以通過`Illuminate\Http\Request`實例來訪問已認證的用戶。別忘了,類型提示的類會被自動注入到你的控制器方法中:
~~~php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ProfileController extends Controller
{
/**
* 更新用戶簡介
*
* @param Request $request
* @return Response
*/
public function update(Request $request)
{
// $request->user() 返回已認證的用戶的實例...
}
}
~~~
#### 確認當前用戶是否被認證
你可以使用`Auth`facade 的`check`方法來檢查用戶是否已認證。如果已認證,將會返回`true`:
~~~php
use Illuminate\Support\Facades\Auth;
if (Auth::check()) {
// 用戶已認證...
}
~~~
> {提示} 雖然可以使用`check`方法確認用戶是否被認證,但是在允許用戶訪問的某些路由 / 控制器之前,通常還是會使用中間件來驗證用戶是否進行過身份驗證。想要了解更多信息,請查看有關[路由保護](https://laravel-china.org/docs/laravel/5.7/authentication#protecting-routes)的文檔。
### 保護路由
[路由中間件](https://laravel-china.org/docs/laravel/5.7/middleware)可以用于只允許通過認證的用戶訪問給定的路由。Laravel 自帶了一個`auth`中間件, 它定義于`Illuminate\Auth\Middleware\Authenticate`。由于這個中間件已經在HTTP 內核中注冊,你所有需要做的只是把這個中間件附加到路由定義中:
~~~php
Route::get('profile', function () {
// 只有認證過的用戶可以進入...
})->middleware('auth');
~~~
當然,如果你使用[控制器](https://laravel-china.org/docs/laravel/5.7/controllers),你可以在控制器的構造函數中調用`middleware`方法來直接將其附加到路由定義中:
~~~php
public function __construct()
{
$this->middleware('auth');
}
~~~
#### 重定向未認證的用戶
當`auth`中間件檢測到一個未認證用戶時,它將返回一個`401`的JSON響應,或者,如果不是 AJAX 請求, 重定向用戶到名為`login`的[命名路由](https://laravel-china.org/docs/laravel/5.7/routing#named-routes).
你可以在`app/Exceptions/Handler.php`文件中定義一個`unauthenticated`方法來修改這個行為:
~~~php
use Illuminate\Auth\AuthenticationException;
protected function unauthenticated($request, AuthenticationException $exception)
{
return $request->expectsJson()
? response()->json(['message' => $exception->getMessage()], 401)
: redirect()->guest(route('login'));
}
~~~
#### 指定看守器
當你把`auth`中間件添加到路由中時,同時也能指定使用哪個看守器進行用戶認證。指定的看守器應該對應你的`auth.php`中`guards`數組的一個鍵 :
~~~php
public function __construct()
{
$this->middleware('auth:api');
}
~~~
### 登錄限制
如果你使用 Laravel 內置的`LoginController`類,`Illuminate\Foundation\Auth\ThrottlesLogins`trait 已經包含在控制器中。默認的,如果用戶經過多次的登錄嘗試,依然無法提供正確的登錄憑據,那么這個用戶在一分鐘內將不能再次嘗試登錄。這基于用戶的用戶名/郵箱地址和他的 IP 地址形成的特定限制。
## 手動認證用戶
當然,你不一定需要使用 Laravel 內置的認證控制器。如果你選擇刪除這些控制器,你將需要使用 Laravel 的認證類直接進行用戶認證的管理。別擔心,這很簡單 !
我們將使用`Auth`[facade](https://laravel-china.org/docs/laravel/5.7/facades)來訪問 Laravel 的認證服務,所以我們需要確保在類的頂部引入`Auth`facade。接下來,讓我們看看`attempt`方法:
~~~php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
/**
* 處理認證嘗試
*
* @param \Illuminate\Http\Request $request
*
* @return Response
*/
public function authenticate(Request $request)
{
$credentials = $request->only('email', 'password');
if (Auth::attempt($credentials)) {
// Authentication passed...
return redirect()->intended('dashboard');
}
}
}
~~~
`attempt`方法接受一個鍵值對數組作為其第一個參數。數組中的值將被用于在數據表中查找用戶。所以,在上例中,用戶將通過`email`字段的值進行檢索。如果找到了這個用戶,數據庫中保存的哈希密碼將被用來與傳遞給方法的數組中`password`的哈希值進行比較。你不應該將指定為密碼的`password`的值進行哈希操作,因為框架將在比較前,自動對其進行hash操作。如果這兩個哈希密碼匹配,就會為用戶開啟一個已認證的會話。
如果認證成功那么`attempt`方法將返回`true`。反之,會返回`false`。
重定向器上的`intended`方法將重定向用戶到他們曾經希望訪問的 URL,這個 URL 之前被用戶認證中間件攔截了。可以給這個方法傳遞一個回退 URI,用于預期的地址不可用的情況。
#### 指定額外條件
如果您愿意,除了用戶的電子郵件和密碼之外,您還可以為身份認證查詢添加額外的條件。例如,我們可以驗證用戶是否標記為「激活」狀態:
~~~php
if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {
// 用戶是激活狀態,沒有被暫停,而且存在
}
~~~
> {note} 在這些例子中`email`不是必需的選項,它僅用作示例。你應該使用數據庫中任何與「用戶名」對應的字段的名稱。
#### 訪問指定的看守器實例
您可以使用`Auth`facade 上的`guard`方法指定要使用的看守器實例。這允許你使用完全獨立的可認證模型或用戶表來管理應用程序的單獨部分的身份驗證。
傳遞給`guard`方法的的看守器名稱,應該與你的`auth.php`配置文件中配置的某個值對應:
~~~php
if (Auth::guard('admin')->attempt($credentials)) {
//
}
~~~
#### 注銷登錄
要將用戶從應用中注銷,你可以使用`Auth`facade 上的`logout`方法。這將清除用戶會話中的認證信息:
~~~php
Auth::logout();
~~~
### 記住用戶
如果你想在應用中提供「記住我」的功能 ,你可以給`attempt`方法的第二個參數傳入一個布爾值,這將永久保持用戶的認證狀態,或者直到他們手動注銷登錄。當然,你的`users`表必須包含名為`remember_token`的字符串字段,它將被用于保存「記住我」的令牌。
~~~php
if (Auth::attempt(['email' => $email, 'password' => $password], $remember)) {
// 這個用戶被記住了...
}
~~~
> {tip} 如果你使用的是 Laravel 附帶的內置`LoginController`,「記住」用戶的恰當邏輯已經由控制器中的 trait 實現。
如果你啟用了「記住」用戶,你可以使用`viaRemember`方法確定用戶是否正使用「記住我」的cookie令牌進行的認證 :
~~~php
if (Auth::viaRemember()) {
//
}
~~~
### 其它認證方式
#### 驗證用戶實例
如果你需要把現存的用戶實例登入應用, 你可以調用`login`方法并附帶用戶實例作為參數。給定的這個對象必須實現`Illuminate\Contracts\Auth\Authenticatable`[契約](https://laravel-china.org/docs/laravel/5.7/contracts)。 當然,Laravel自帶的`App\User`模型已經實現了這個接口:
~~~php
Auth::login($user);
// 登錄并且「記住」給定的用戶...
Auth::login($user, true);
~~~
當然,你可以指定想要使用的看守器實例:
~~~php
Auth::guard('admin')->login($user);
~~~
#### 通過 ID 驗證用戶
要使用用戶的 ID 登錄應用,你可以使用`loginUsingId`方法。這個方法接受需要認證的用戶主鍵:
~~~php
Auth::loginUsingId(1);
// 登錄并且「記住」給定的用戶...
Auth::loginUsingId(1, true);
~~~
#### 僅驗證用戶一次
你可以在單次請求中使用`once`方法將用戶登錄到應用中。這將不會使用任何 Session 或者 Cookie,這意味著在構建無狀態API時,此方法可能會有所幫助:
~~~php
if (Auth::once($credentials)) {
//
}
~~~
## HTTP 基礎認證
[HTTP 基礎認證](https://en.wikipedia.org/wiki/Basic_access_authentication)提供了一種快速方法來驗證你應用程序中的用戶,而無需設置專用的「登錄」頁面。 開始之前, 先把`auth.basic`[中間件](https://laravel-china.org/docs/laravel/5.7/middleware)附加到你的路由中。`auth.basic`中間件已包含在 Laravel 框架中,所以你不需要定義它:
~~~php
Route::get('profile', function () {
// 只有認證過的用戶可以進入...
})->middleware('auth.basic');
~~~
將中間件附加到路由后,在瀏覽器中訪問此路由時將自動提示您輸入憑據。默認的,`auth.basic`中間件把用戶記錄上的`email`字段 作為「用戶名」。
#### FastCGI 的注意事項
如果你正使用 PHP FastCGI 模式,HTTP 基礎認證可能無法正常工作。需要把下面幾行添加到你的`.htaccess`文件中:
~~~php
RewriteCond %{HTTP:Authorization} ^(.+)$
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
~~~
### 無狀態 HTTP 基礎認證
你也可以使用 HTTP 基礎身份驗證,而無需在會話中設置用戶標識符cookie,這對 API 的身份驗證特別有用。為此 ,請[定義一個中間件](https://laravel-china.org/docs/laravel/5.7/middleware)它將調用`onceBasic`方法。如果`onceBasic`方法沒有返回任何響應,那么請求就可以進一步傳遞到應用程序中:
~~~php
<?php
namespace App\Http\Middleware;
use Illuminate\Support\Facades\Auth;
class AuthenticateOnceWithBasicAuth
{
/**
* 處理傳入的請求
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, $next)
{
return Auth::onceBasic() ?: $next($request);
}
}
~~~
接著,[注冊路由中間件](https://laravel-china.org/docs/laravel/5.7/middleware#registering-middleware)并將它附加到路由:
~~~php
Route::get('api/user', function () {
// 只有認證過的用戶可以進入...
})->middleware('auth.basic.once');
~~~
## 退出
要手動把用戶從應用中退出登錄,你可以使用`Auth`facade 上的`logout`方法。這將清除用戶會話中的身份認證信息:
~~~php
use Illuminate\Support\Facades\Auth;
Auth::logout();
~~~
### 讓其它設備上的 Session 失效
Laravel 還提供了一種機制,用于將其它設備上的用戶 Session 失效和「注銷」,而不會使其當前設備上的 Session 失效。首先,你需要保證`Illuminate\Session\Middleware\AuthenticateSession`中間件在你的`app/Http/Kernel.php`類中的`web`中間件組中,并且沒有被注釋掉:
~~~php
'web' => [
// ...
\Illuminate\Session\Middleware\AuthenticateSession::class,
// ...
],
~~~
然后, 你就可以使用`Auth`facade 上的`logoutOtherDevices`方法。此方法要求用戶提供其當前密碼,你的應用程序應通過輸入表單接受該密碼:
~~~php
use Illuminate\Support\Facades\Auth;
Auth::logoutOtherDevices($password);
~~~
> {note} 當調用`logoutOtherDevices`方法后,用戶的其它 Session 將完全失效,這意味著他們將「退出」他們之前通過身份認證的所有看守器。
## 添加自定義的看守器
你可以使用`Auth`facade 的`extend`方法來定義自己的身份驗證看守器。你應該在[服務提供器](https://laravel-china.org/docs/laravel/5.7/providers)中調用`extend`方法。由于 Laravel 已經附帶了`AuthServiceProvider`,我們可以將代碼放在該提供器中:
~~~php
<?php
namespace App\Providers;
use App\Services\Auth\JwtGuard;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* 注冊任意應用認證/授權服務。
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Auth::extend('jwt', function ($app, $name, array $config) {
// 返回一個 Illuminate\Contracts\Auth\Guard 實例...
return new JwtGuard(Auth::createUserProvider($config['provider']));
});
}
}
~~~
正如你在上面的示例中所看到的,傳遞給`extend`方法的回調應該返回一個實現`Illuminate\Contracts\Auth\Guard`接口的實例。這個接口包含了一些你需要在自定義的看守器中實現的方法。當你的自定義看守器定義完成之后,你可以在`auth.php`配置文件的`guards`配置中使用這個看守器:
~~~php
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
~~~
### 請求閉包看守器
實現基于HTTP請求的自定義身份驗證系統的最簡單方法,是使用`Auth::viaRequest`方法。此方法允許您使用單個閉包來快速定義身份驗證過程。
首先,在`AuthServiceProvider`的`boot`方法中調用`Auth::viaRequest`方法。`viaRequest`方法接受一個看守器名稱作為其第一個參數。此名稱可以是描述你自定義看守器的任何字符串。傳遞給該方法的第二個參數應該是一個閉包函數,它接收傳入的HTTP請求并返回一個用戶實例,或者,如果驗證失敗,則為`null`:
~~~php
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
/**
* 注冊任意應用認證/授權服務。
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Auth::viaRequest('custom-token', function ($request) {
return User::where('token', $request->token)->first();
});
}
~~~
當你完成了自定義看守器后,就可以在`auth.php`配置文件的`guards`配置中使用這個看守器:
~~~php
'guards' => [
'api' => [
'driver' => 'custom-token',
],
],
~~~
## 添加自定義用戶提供器
如果您沒有使用傳統的關系型數據庫來存儲用戶,則需要使用自己的身份驗證用戶提供器來擴展 Laravel。我們將在`Auth`facade上使用`provider`方法來自定義一個用戶提供器:
~~~php
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Auth;
use App\Extensions\RiakUserProvider;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* 注冊任何應用認證/授權服務。
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Auth::provider('riak', function ($app, array $config) {
// Return an instance of Illuminate\Contracts\Auth\UserProvider...
return new RiakUserProvider($app->make('riak.connection'));
});
}
}
~~~
使用`provider`方法注冊提供器后,可以在`auth.php`配置文件中切換到新的用戶提供器。首先,定義一個使用新驅動的`provider`:
~~~php
'providers' => [
'users' => [
'driver' => 'riak',
],
],
~~~
最后,你可以在`guards`配置中使用此提供器:
~~~php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
~~~
### 用戶提供器契約
`Illuminate\Contracts\Auth\UserProvider`的實現只負責從永久存儲系統(如 MySQL、Riak 等)中獲取`Illuminate\Contracts\Auth\Authenticatable`的實現實例。無論用戶數據的存儲方式或用于表示用戶數據的類是什么,這兩個接口都允許 Laravel 身份驗證機制繼續運行。
讓我們來看看`Illuminate\Contracts\Auth\UserProvider`契約:
~~~php
<?php
namespace Illuminate\Contracts\Auth;
interface UserProvider {
public function retrieveById($identifier);
public function retrieveByToken($identifier, $token);
public function updateRememberToken(Authenticatable $user, $token);
public function retrieveByCredentials(array $credentials);
public function validateCredentials(Authenticatable $user, array $credentials);
}
~~~
`retrieveById`函數通常接收代表用戶的鍵,例如來自 MySQL 數據庫的自動遞增 ID。同時,應該檢索并返回與 ID 匹配的`Authenticatable`實現。
`retrieveByToken`函數通過用戶唯一的`$identifier`和「記住我」`$token`來檢索用戶,后者存儲在字段`remember_token`中。與前面的方法一樣,它應該返回`Authenticatable`的實現實例。
`updateRememberToken`方法用新的`$token`更新`$user`的`remember_token`字段。在成功使用「記住我」登錄后或用戶注銷時分配新令牌。
在嘗試登錄應用程序時,`retrieveByCredentials`方法接收`Auth::attempt`方法傳遞的憑證數組。然后該方法應該「查詢」底層持久存儲中匹配這些憑據的用戶。 通常,此方法將使用`$credentials['username']`執行一個「where」查詢。這個方法應該返回一個`Authenticatable`的實現實例。**此方法不應嘗試進行任何密碼驗證或身份驗證。**
`validateCredentials`方法應該將給定的`$user`與`$credentials`進行比較,來驗證用戶。例如,這個方法可能應該使用`Hash::check`來比較`$user->getAuthPassword()`和`$credentials['password']`的值。這個方法應該返回`true`或者`false`來指示密碼是否有效。
### 認證契約
現在我們已經探討了`UserProvider`上的每個方法,讓我們來看看`Authenticatable`契約。記住,提供器中`retrieveById`,`retrieveByToken`和`retrieveByCredentials`方法應該返回此接口的實例:
~~~php
<?php
namespace Illuminate\Contracts\Auth;
interface Authenticatable {
public function getAuthIdentifierName();
public function getAuthIdentifier();
public function getAuthPassword();
public function getRememberToken();
public function setRememberToken($value);
public function getRememberTokenName();
}
~~~
這個接口很簡單。`getAuthIdentifierName`方法應該返回用戶「主鍵」字段的名稱,而`getAuthIdentifier`應該返回用戶主鍵的值 。重申一次,在 MySQL 后臺,這個主鍵是指自增的主鍵。`getAuthPassword`應該返回用戶的哈希密碼。 此接口允許身份驗證系統與任何的 User 類一起使用,無論你使用的是什么ORM或存儲抽象層。默認情況下,Laravel 的`app`目錄中包含一個`User`類,它實現了這個接口,所以你可以參考這個類來獲得一個實現例子。
## 事件
Laravel 在身份認證過程中引發了各種[事件](https://laravel-china.org/docs/laravel/5.7/events)。你可以在`EventServiceProvider`中為這些事件添加監聽器:
~~~php
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'Illuminate\Auth\Events\Registered' => [
'App\Listeners\LogRegisteredUser',
],
'Illuminate\Auth\Events\Attempting' => [
'App\Listeners\LogAuthenticationAttempt',
],
'Illuminate\Auth\Events\Authenticated' => [
'App\Listeners\LogAuthenticated',
],
'Illuminate\Auth\Events\Login' => [
'App\Listeners\LogSuccessfulLogin',
],
'Illuminate\Auth\Events\Failed' => [
'App\Listeners\LogFailedLogin',
],
'Illuminate\Auth\Events\Logout' => [
'App\Listeners\LogSuccessfulLogout',
],
'Illuminate\Auth\Events\Lockout' => [
'App\Listeners\LogLockout',
],
'Illuminate\Auth\Events\PasswordReset' => [
'App\Listeners\LogPasswordReset',
],
];
~~~