簡介
注:想要快速上手?只需要在新安裝的 Laravel 應用下運行
php artisan make:auth
和php artisan migrate
,這兩個命令會生成用戶登錄注冊所需要的所有東西,然后在瀏覽器中訪問http://your-app.test/register
即可。
Laravel 中實現登錄認證非常簡單。實際上,幾乎所有東西 Laravel 都已經為你配置好了。配置文件位於 config/auth.php
,其中包含了用於調整認證服務行為的、文檔友好的選項配置。
在底層代碼中,Laravel 的認證組件由“guards”和“providers”組成,Guard 定義了用戶在每個請求中如何實現認證,例如,Laravel 通過 session
guard 來維護 Session 存儲的狀態和 Cookie。
Provider 定義了如何從持久化存儲中獲取用戶信息,Laravel 底層支持通過 Eloquent 和數據庫查詢構建器兩種方式來獲取用戶,如果需要的話,你還可以定義額外的 Provider。
如果看到這些名詞覺得雲里霧里,大可不必太過擔心,因為對絕大多數應用而言,只需使用默認認證配置即可,不需要做什么改動。
注:通俗點說,在進行登錄認證的時候,要做兩件事,一個是從數據庫存取用戶數據,一個是把用戶登錄狀態保存起來,在 Laravel 的底層實現中,通過 Provider 存取數據,通過 Guard 存儲用戶認證信息,前者主要和數據庫打交道,后者主要和 Session 打交道(API 例外)。
數據庫考量
默認情況下,Laravel 在 app
目錄下包含了一個 Eloquent 模型 App\User
,這個模型可以和默認的 Eloquent 認證驅動一起使用。如果你的應用不使用 Eloquent,你可以使用 database
認證驅動,該驅動使用 Laravel 查詢構建器與數據庫交互。
為 App\User
模型構建數據庫表結構的時候,確保 password
字段長度至少有60位。保持默認字符串長度(255)是個不錯的選擇。
還有,你需要驗證 users
表包含了 remember_token
,該字段是個可以為空的字符串類型,字段長度為100,用於在登錄時存儲應用維護的“記住我” Session 令牌。
快速入門
Laravel 提供了幾個預置的認證控制器,位於 App\Http\Controllers\Auth
命名空間下, RegisterController
用於處理新用戶注冊, LoginController
用於處理用戶登錄認證, ForgotPasswordController
用於處理重置密碼郵件鏈接, ResetPasswordController
包含重置密碼邏輯,每個控制器都使用 trait 來引入它們需要的方法。對很多應用而言,你根本不需要修改這些控制器:
路由
Laravel 通過運行如下命令可快速生成認證所需要的路由和視圖:
php artisan make:auth
新安裝的應用運行該命令會生成布局、注冊和登錄視圖,以及所有的認證路由,同時生成 HomeController
用於處理應用的登錄請求。
打開 routes/web.php
路由文件會發現新增了兩行:
登錄注冊相關路由都定義在上面 Auth::routes()
方法內。
注:如果你的應用不需要注冊,可以通過移除新創建的
RegisterController
控制器並編輯路由定義來禁止注冊:Auth::routes(['register' => false]);
。
視圖
正如上面所提到的,php artisan make:auth
命令會在 resources/views/auth
目錄下創建所有認證需要的視圖。
make:auth
命令還創建了 resources/views/layouts
目錄,該目錄下包含了應用的基礎布局文件。所有這些視圖都使用了 Bootstrap CSS 框架,你也可以根據需要對其進行自定義。
認證
現在你已經為自帶的認證控制器設置好了路由和視圖,接下來我們來實現新用戶注冊和登錄認證。你可以在瀏覽器中訪問定義好的路由,認證控制器默認已經包含了注冊及登錄邏輯(通過trait)。
我們先來注冊一個新用戶,在瀏覽器中訪問 http://blog.test/register
,即可進入注冊頁面:
填寫表單點擊「Register」按鈕即可完成注冊。注冊成功后頁面跳轉到認證后的頁面 http://blog.test/home
:
要測試登錄功能,可以先退出當前用戶,然后訪問登錄頁面 http://blog.test/login
:
使用我們之前注冊的信息登錄成功后,同樣會跳轉到 http://blog.test/home
。
自定義路徑
我們已經知道,當一個用戶成功進行登錄認證后,默認將會跳轉到 /home
,你可以通過在 LoginController
、RegisterController
和 ResetPasswordController
中定義 redirectTo
屬性來自定義登錄認證成功之后的跳轉路徑:
protected $redirectTo = '/';
接下來,你需要編輯 RedirectIfAuthenticated
中間件的 handle
方法來使用新的重定向 URI。
如果重定向路徑需要自定義生成邏輯可以定義一個 redirectTo
方法來取代 redirectTo
屬性:
protected function redirectTo() { return '/path'; }
注:
redirectTo
方法優先級大於redirectTo
屬性。
自定義用戶名
默認情況下,Laravel 使用 email
字段進行認證,如果你想要自定義認證字段,可以在 LoginController
中定義 username
方法:
public function username() { return 'username'; }
自定義 Guard
你還可以自定義用於實現用戶注冊登錄的“guard”,要實現這一功能,需要在 LoginController
、RegisterController
和 ResetPasswordController
中定義 guard
方法,該方法將會返回一個 guard 實例:
use Illuminate\Support\Facades\Auth; protected function guard() { return Auth::guard('guard-name'); }
需要注意的是,「guard」名稱需要在配置文件 config/auth.php
中配置過:
自定義驗證/存儲
要修改新用戶注冊所必需的表單字段,或者自定義新用戶字段如何存儲到數據庫,你可以修改 RegisterController
類。該類負責為應用驗證輸入參數和創建新用戶。
RegisterController
的 validator
方法包含了新用戶注冊的驗證規則,你可以按需要自定義該方法。
RegisterController
的 create
方法負責使用 Eloquent ORM 在數據庫中創建新的 App\User
記錄。當然,你也可以基於自己的需要自定義該方法。
獲取登錄用戶
你可以通過 Auth
門面訪問認證用戶:
use Illuminate\Support\Facades\Auth; // 獲取當前認證用戶... $user = Auth::user(); // 獲取當前認證用戶的ID... $id = Auth::id();
此外,用戶通過認證后,你還可以通過 Illuminate\Http\Request
實例訪問認證用戶(類型提示類會通過依賴注入自動注入到控制器方法中):
<?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
門面的 check
方法,如果用戶通過認證則返回 true
:
use Illuminate\Support\Facades\Auth; if (Auth::check()) { // The user is logged in... }
注:盡管我們可以使用
check
方法判斷用戶是否通過認證,但是我們通常的做法是在用戶訪問特定路由/控制器之前使用中間件來驗證用戶是否通過認證。
路由保護
路由中間件可用於只允許通過認證的用戶訪問給定路由。Laravel 通過定義在 Illuminate\Auth\Middleware\Authenticate
中的 auth
中間件來實現這一功能。由於該中間件已經在 HTTP kernel 中注冊,你所要做的僅僅是將該中間件加到相應的路由定義中:
Route::get('profile', function() { // 只有認證用戶可以進入... })->middleware('auth');
當然,如果你也可以在控制器的構造方法中調用 middleware
方法而不是在路由器中直接定義實現同樣的功能:
public function __construct(){ $this->middleware('auth'); }
比如我們的 HomeController
就是這么做的:
如果我們在沒有登錄的情況下訪問 http://blog.test/home
頁面就會重定向到登錄頁面。
重定向未認證用戶
當 auth
中間件判定某個用戶未認證,會返回一個 JSON 401
響應,或者,如果不是 Ajax 請求的話,將用戶重定向到 login
命名路由(也就是登錄頁面)。
你可以通過更新 app/Http/Middleware/Authenticate.php
文件中的 redirectTo
函數來改變這一行為:
/** * Get the path the user should be redirected to. * * @param \Illuminate\Http\Request $request * @return string */ protected function redirectTo($request) { return route('login'); }
指定一個 Guard
添加 auth
中間件到路由后,還可以指定使用哪個 guard 來實現認證, 指定的 guard 對應配置文件 config/auth.php
中 guards
數組的某個鍵 :
public function __construct() { $this->middleware('auth:api'); }
如果沒有指定的話,默認 guard 是 web
,這也是配置文件中配置的:
登錄失敗次數限制
如果你使用了 Laravel 自帶的 LoginController
類, 就已經啟用了內置的 Illuminate\Foundation\Auth\ThrottlesLogins
trait 來限制用戶登錄失敗次數。默認情況下,用戶在幾次登錄失敗后將在一分鍾內不能登錄,這種限制基於用戶的用戶名/郵箱地址+IP地址作為唯一鍵。
手動認證用戶
當然,你也可以不使用 Laravel 自帶的認證控制器。如果你選擇移除這些控制器,需要直接使用 Laravel 認證類來管理用戶認證。別擔心,這很簡單!
我們可以通過 Auth
門面來訪問認證服務,因此我們需要確保在類的頂部導入了 Auth
門面,接下來,讓我們看看如何通過 attempt
方法實現登錄認證:
<?php namespace App\Http\Controllers; use Illuminate\Support\Facades\Auth; class LoginController extends Controller { /** * 處理登錄認證 * * @return Response * @translator laravelacademy.org */ public function authenticate() { if (Auth::attempt(['email' => $email, 'password' => $password])) { // 認證通過... return redirect()->intended('dashboard'); } } }
attempt
方法接收鍵/值對作為第一個參數,數組中的值被用於從數據表中查找對應用戶。在上面的例子中,將會通過 email
的值作為查詢條件去數據庫獲取對應用戶,如果用戶被找到,經哈希運算后存儲在數據庫中的密碼將會和傳遞過來的經哈希運算處理的密碼值進行比較。如果兩個經哈希運算的密碼相匹配,那么將會為這個用戶設置一個認證 Session,標識該用戶登錄成功。感興趣的同學可以去看下底層源碼實現邏輯:
如果認證成功的話 attempt
方法將會返回 true
。否則,返回 false
。
重定向器上的 intended
方法將用戶重定向到登錄之前用戶想要訪問的 URL,在目標 URL 無效的情況下回退 URI 將會傳遞給該方法。
指定額外條件
如果需要的話,除了用戶郵件和密碼之外還可以在認證查詢時添加額外的條件,例如,我們可以驗證被標記為有效的用戶:
if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) { // The user is active, not suspended, and exists. }
這里的實現原理是在查詢用戶記錄時,只是排除了數組中的密碼字段,其他字段都會作為查詢條件之一進行篩選:
注:在這些例子中,並不僅僅限於使用
訪問指定 Guard 實例
你可以使用 Auth
門面的 guard
方法指定想要使用的 guard 實例,這種機制允許你在同一個應用中對不同的認證模型或用戶表實現完全獨立的用戶認證。
該功能可用於為不同表的不同類型用戶(同一個表不同類型用戶理論上也可以)實現隔離式登錄提供了方便,我們只要為每張表配置一個獨立的 guard 就可以了。比如我們除了 users
表之外還有一張 admins
表用於存放后台管理員,要實現管理員的單獨登錄,就可以這么配置 auth.php
配置文件:
'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'token', 'provider' => 'users', ], 'admin' => [ 'driver' => 'session', 'provider' => 'admins', ] ], 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\User::class, ], 'admins' => [ 'driver' => 'eloquent', 'model' => App\Admin::class, ], ],
友情提示:新建的用於登錄認證的模型類需要繼承
Illuminate\Foundation\Auth\User
基類,不然后面就會出現不能認證的窘況。
傳遞給 guard
方法的 guard 名稱對應配置文件 auth.php
中 guards
配置的 admin
鍵:
if (Auth::guard('admin')->attempt($credentials)) { // }
需要注意的是使用這種方式認證的用戶在后續操作需要傳遞 guard 時也要傳遞相匹配的 guard,比如上面提到的 auth
中間件,對應的調用方式也要調整(在路由中使用也是一樣):
$this->middleware('auth:admin');
獲取用戶時也是一樣:
Auth::guard('admin')->user();
退出
要退出應用,可以使用 Auth
門面的 logout
方法,這將會清除用戶 Session 中的認證信息:
Auth::logout();
記住用戶
如果你想要在應用中提供“記住我”的功能,可以傳遞一個值為 true
的布爾值作為第二個參數到 attempt
方法(不傳的話默認是 false
),這樣用戶登錄認證狀態就會一直保持直到他們手動退出。當然,你的 users
表必須包含 remember_token
字段,該字段用於存儲“記住我”令牌。
if (Auth::attempt(['email' => $email, 'password' => $password], $remember)) { // The user is being remembered... }
注:如果你在使用自帶的
LoginController
控制器,相應的記住用戶邏輯已經通過控制器使用的 trait 實現了。
如果你在使用“記住”用戶功能,可以使用 viaRemember
方法來判斷用戶是否通過“記住我”Cookie進行認證:
if (Auth::viaRemember()) { // }
其它認證方法
認證一個用戶實例
如果你需要將一個已存在的用戶實例直接登錄到應用,可以調用 Auth
門面的 login
方法並傳入用戶實例,傳入實例必須是 Illuminate\Contracts\Auth\Authenticatable
契約的實現,當然,Laravel 自帶的 App\User
模型已經實現了該接口:
Auth::login($user); // 登錄並 "記住" 給定用戶... Auth::login($user, true);
當然,你可以指定想要使用的 guard 實例:
Auth::guard('admin')->login($user);
通過 ID 認證用戶
要通過用戶ID登錄到應用,可以使用 loginUsingId
方法,該方法接收你想要認證用戶的主鍵作為參數:
Auth::loginUsingId(1); // 登錄並 "記住" 給定用戶... Auth::loginUsingId(1, true);
一次性認證用戶
你可以使用 once
方法只在單個請求中將用戶登錄到應用,而不存儲任何 Session 和 Cookie,這在構建無狀態的 API 時很有用:
if (Auth::once($credentials)) { // }
基於 HTTP 的基本認證
HTTP 基本認證能夠幫助用戶快速實現登錄認證而不用設置專門的登錄頁面,首先要在路由中加上 auth.basic
中間件。該中間件是 Laravel 自帶的,所以不需要自己定義:
Route::get('profile', function() { // 只有認證用戶可以進入... })->middleware('auth.basic');
中間件加到路由中后,當在瀏覽器中訪問該路由時,會自動提示需要認證信息,默認情況下,auth.basic
中間件使用用戶記錄上的 email
字段作為「用戶名」。
學院君注:這種基本認證除了沒有獨立的登錄表單視圖之外底層實現邏輯和正常的登錄認證沒有區別。
FastCGI 上的注意點
如果你使用 PHP FastCGI,HTTP 基本認證將不能正常工作,需要在 .htaccess
文件加入如下內容:
RewriteCond %{HTTP:Authorization} ^(.+)$
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
無狀態的 HTTP 基本認證
你也可以在使用 HTTP 基本認證時不在 Session 中設置用戶標識 Cookie,這在 API 認證中非常有用。要實現這個功能,需要定義一個調用 onceBasic
方法的中間件。如果該方法沒有返回任何響應,那么請求會繼續走下去:
<?php namespace Illuminate\Auth\Middleware; use Illuminate\Support\Facades\Auth; class AuthenticateOnceWithBasicAuth { /** * 處理輸入請求. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed * @translator laravelacademy.org */ public function handle($request, $next) { return Auth::onceBasic() ?: $next($request); } }
接下來,將 AuthenticateOnceWithBasicAuth
注冊到路由中間件並在路由中使用它:
Route::get('api/user', function() { // 只有認證用戶可以進入... })->middleware('auth.basic.once');
退出
如果要手動將用戶退出應用,可以使用 Auth
門面上的 logout
方法,該方法會清空用戶 Session 中的認證信息:
use Illuminate\Support\Facades\Auth; Auth::logout();
讓其他設備上的 Session 失效
Laravel 還提供了讓用戶 Session 在除當前設備之外的其他登錄設備上失效的機制,要實現這個功能,需要確保 Illuminate\Session\Middleware\AuthenticateSession
中間件在 app/Http/Kernel.php
類的 web
中間件組中存在且沒有被注釋:
'web' => [ // ... \Illuminate\Session\Middleware\AuthenticateSession::class, // ... ],
然后,你可以使用 Auth
門面上的 logoutOtherDevices
方法實現在其他設備「退出」,該方法要求用戶提供登錄密碼:
use Illuminate\Support\Facades\Auth; Auth::logoutOtherDevices($password);
注:當
logoutOtherDevices
方法被調用時,用戶在其他設備的 Session 會完全失效,表現在用戶界面上就是退出登錄了。
添加自定義 Guard 驅動
你可以通過 Auth
門面的 extend
方法定義自己的認證 guard 驅動,該功能需要在某個服務提供者的 boot
方法中實現,由於 Laravel 已經自帶了一個 AuthServiceProvider
,所以我們將代碼放到這個服務提供者中:
<?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
的實現實例,該接口包含了自定義認證 guard 驅動需要的一些方法。定義好自己的認證 guard 驅動之后,就可以在配置文件 auth.php
的 guards
配置中使用這個新的 guard 驅動:
'guards' => [ 'api' => [ 'driver' => 'jwt', 'provider' => 'users', ], ],
閉包請求 Guard
實現一個自定義的、基於 HTTP 請求的認證系統最簡單的方式就是使用 Auth:viaRequest
方法,該方法允許你通過單個閉包快速定義認證流程。
首先我們需要在 AuthServiceProvider
的 boot
方法中調用 Auth::viaRequest
,viaRequest
方法接收一個 guard 名稱作為第一個參數,這個名稱可以是任意描述自定義 guard 的字符串,傳遞到該方法第二個參數應該是一個閉包,該閉包接收 HTTP 請求並返回一個用戶實例,如果認證失敗的話,則返回 null
:
use App\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; /** * Register any application authentication / authorization services. * * @return void */ public function boot() { $this->registerPolicies(); Auth::viaRequest('custom-token', function ($request) { return User::where('token', $request->token)->first(); }); }
定義好自定義 guard 后,就可以在 auth.php
配置文件的配置項 guards
中使用這個 guard 了:
'guards' => [ 'api' => [ 'driver' => 'custom-token', ], ],
添加自定義用戶提供者
如果你沒有使用傳統的關系型數據庫存儲用戶信息,則需要使用自己的認證用戶提供者來擴展 Laravel。我們使用 Auth
門面上的 provider
方法定義自定義該提供者:
<?php namespace App\Providers; use Illuminate\Support\Facades\Auth; use App\Extensions\RiakUserProvider; use Illuminate\Support\ServiceProvider; class AuthServiceProvider extends ServiceProvider { /** * 注冊任意應用認證/授權服務. * * @return void */ public function boot() { $this->registerPolicies(); Auth::provider('riak', function($app, array $config) { // 返回一個Illuminate\Contracts\Auth\UserProvider實例... return new RiakUserProvider($app->make('riak.connection')); }); } }
通過 provider
方法注冊用戶提供者后,你可以在配置文件 config/auth.php
中切換到新的用戶提供者。首先,定義一個使用新驅動的 provider
:
'providers' => [ 'users' => [ 'driver' => 'riak', ], ],
然后,可以在你的 guards
配置中使用這個提供者:
'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], ],
User Provider 契約
Illuminate\Contracts\Auth\UserProvider
實現只負責從持久化存儲系統中獲取 Illuminate\Contracts\Auth\Authenticatable
實現,例如 MySQL、Riak 等等。這兩個接口允許 Laravel 認證機制繼續起作用而不管用戶數據如何存儲或者何種類來展現。
讓我們先看看 Illuminate\Contracts\Auth\UserProvider
契約:
<?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
函數通過唯一標識和存儲在 remember_token
字段中的“記住我”令牌獲取用戶。和上一個方法一樣,該方法也返回 Authenticatable
實現。
updateRememberToken
方法使用新的 $token
更新 $user
的 remember_token
字段,新令牌可以是新生成的令牌(在登錄是選擇“記住我”被成功賦值)或者null
(用戶退出)。
retrieveByCredentials
方法在嘗試登錄系統時獲取傳遞給 Auth::attempt
方法的認證信息數組。該方法接下來去底層持久化存儲系統查詢與認證信息匹配的用戶,通常,該方法運行一個帶“where”條件($credentials[‘username’]
)的查詢。然后該方法返回 Authenticatable
的實現。這個方法不應該做任何密碼校驗和認證。
validateCredentials
方法比較給定 $user
和 $credentials
來認證用戶。例如,這個方法比較 $user->getAuthPassword()
字符串和經 Hash::check
處理的 $credentials['password']
。這個方法根據密碼是否有效返回布爾值 true
或 false
。
Authenticatable 契約
既然我們已經探索了 UserProvider
上的每一個方法,接下來讓我們看看 Authenticatable
。記住,提供者需要從 retrieveById
和 retrieveByCredentials
方法中返回接口實現:
<?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 中這將是自增ID,getAuthPassword
返回經哈希處理的用戶密碼,這個接口允許認證系統處理任何用戶類,不管是你使用的是 ORM 還是存儲抽象層。默認情況下,Laravel app
目錄下的 User
類實現了這個接口,所以你可以將這個類作為實現例子。
事件
Laravel 支持在認證過程中觸發多種事件,你可以在自己的 EventServiceProvider
中監聽這些事件:
/** * 應用的事件監聽器映射. * * @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', ], ];