Authentication 認(rèn)證系統(tǒng)使用
Authentication
學(xué)習(xí)筆記《Laravel Auth 代碼閱讀》
花了兩天研究 Laravel,昨天是通宵達(dá)旦搞到將近凌晨5點(diǎn),基本掌握系統(tǒng)架構(gòu),可以說是博大精深!今天繼續(xù),主要心思放到整個(gè) Laravel Auth 子系統(tǒng)中來了。
基本流程解讀
HTTP本身是無狀態(tài),通常在系統(tǒng)交互的過程中,使用賬號(hào)或者Token標(biāo)識(shí)來確定認(rèn)證用戶,Token 一般會(huì)以 Cookie 方式保存到客戶端,客戶端發(fā)送請(qǐng)求時(shí)一并將 Token 發(fā)回服務(wù)器。如果客服端沒有 Cookie,通常做法時(shí)時(shí)在 URL 中攜帶 Token。也可以 HTTP 頭信息方式攜帶 Token 到服務(wù)器。Laravel 提供 Session 和 URL 中攜帶 token 兩種方式做認(rèn)證。對(duì)應(yīng)配置文件中的 guards
配置的 web、api。
web 認(rèn)證基于 Session 根據(jù) SessionId 獲取用戶,provider 查詢關(guān)聯(lián)用戶;api認(rèn)證是基于token值交互,也采用users這個(gè)provider;
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
配置文件 /config/auth.php
中的 provider 是提供用戶數(shù)據(jù)的接口,要標(biāo)注驅(qū)動(dòng)對(duì)象和目標(biāo)對(duì)象,默認(rèn)值 users
是一套 provider 的名字,采用 eloquent 驅(qū)動(dòng),對(duì)應(yīng)模類是 App\User
。也可以使用 database 的方式。
Laravel 內(nèi)置了認(rèn)證模塊,也可以手動(dòng)安裝,命令會(huì)生成相關(guān)的視圖文件:
artisan make:auth
app
+-- Http
| +-- Middleware
| | +-- RedirectIfAuthenticated.php
| +-- Controllers
| +-- Auth
| +-- ForgotPasswordController.php
| +-- LoginController.php
| +-- RegisterController.php
| +-- ResetPasswordController.php
+-- Providers
| +-- AuthServiceProvider.php
+-- User.php
整個(gè)認(rèn)證個(gè)模塊包括相應(yīng)的視圖文件,一個(gè) RedirectIfAuthenticated
中間件和四個(gè) Controller 文件,一個(gè) User 模型文件,它是 Authenticatable
接口類。另外還有一個(gè) AuthServiceProvider
它是服務(wù)容器,為注入認(rèn)證用戶的數(shù)據(jù)提供者 UserProvider
接口準(zhǔn)備的。密碼數(shù)據(jù)是從 Authenticatable
流向 UserProvider
的,前者讀取數(shù)據(jù),后者負(fù)責(zé)校驗(yàn)。列如可以嘗試這樣覆蓋 getAuthPassword()
方法來測(cè)試數(shù)據(jù)邏輯,而不是從數(shù)據(jù)庫(kù)中讀入數(shù)據(jù):
public function getAuthPassword(){
return bcrypt("userpass");
}
此方法原本是在 Illuminate\Auth\Authenticatable
定義的,它是 trait Authenticatable 類,是 PHP 多繼承的規(guī)范。這個(gè)方法就是簡(jiǎn)單地返回 $this->password
,即模型關(guān)聯(lián)的數(shù)據(jù)表字段 password
的值,數(shù)據(jù)是通過依賴注入的。
在 app/Http/Kernel.php
注冊(cè)中間件:
protected $routeMiddleware = [
// ...
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
];
Auth模塊從功能上分為用戶認(rèn)證和權(quán)限管理兩個(gè)部分;
Illuminate\Auth
是負(fù)責(zé)用戶認(rèn)證和權(quán)限管理的模塊;
Illuminate\Foundation\Auth
提供了登錄、修改密碼、重置密碼等一系統(tǒng)列具體邏輯實(shí)現(xiàn);
Illuminate\Foundation\Auth\AuthenticatesUsers
負(fù)責(zé)登錄視圖邏輯
Illuminate\Auth\Passwords
目錄下是密碼重置或忘記密碼處理的小模塊;
config\auth.php
認(rèn)證相關(guān)配置文件
RedirectIfAuthenticated 只有 handle()
處理邏輯,但 Auth 類中并沒有定義 Auth::guard()
,這是經(jīng)過 Facades 編程模式,通過 __callStatic()
關(guān)聯(lián)到了 AuthManager
,代碼注解中有提示。Facade 是一套配合 Service Container 的靜態(tài)方法解決方案,是一套設(shè)計(jì)得非常優(yōu)雅的機(jī)制,是 Laravel 的核心機(jī)制之一。
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect('/home');
}
return $next($request);
}
通過獲取配置文件的 guards 設(shè)置,不同的依賴就會(huì)加載進(jìn)來:
HTTP Basic: /src/Illuminate/Auth/RequestGuard.php
Session: /src/Illuminate/Auth/SessionGuard.php
Token: /src/Illuminate/Auth/TokenGuard.php
這些按標(biāo)準(zhǔn)接口定義的類都有 user()
方法,這個(gè)方法就是從數(shù)據(jù)庫(kù)獲取匹配的授權(quán)用戶,用戶數(shù)據(jù)是通過 UserProvider 接口提供的,即 /app/Providers/AuthServiceProvider.php
。通過實(shí)現(xiàn)此接口可以定制自己的認(rèn)證邏輯,UserProvider::validateCredentials()
就是檢驗(yàn)密碼的方法。自帶的 /app/User.php
模型就是實(shí)現(xiàn)了 Authenticatable
接口的類。
public function user()
{
if ($this->loggedOut) {
return;
}
if (! is_null($this->user)) {
return $this->user;
}
$id = $this->session->get($this->getName());
if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
$this->fireAuthenticatedEvent($this->user);
}
if (is_null($this->user) && ! is_null($recaller = $this->recaller())) {
$this->user = $this->userFromRecaller($recaller);
if ($this->user) {
$this->updateSession($this->user->getAuthIdentifier());
$this->fireLoginEvent($this->user, true);
}
}
return $this->user;
}
在 /Illuminate/Routing/Router.php
中已經(jīng)定義好和認(rèn)證相關(guān)的一組路由,只需在 /routes/web.php
中添加一句 Auth::routes();
就可以注冊(cè)這些路由,這些路由連接的控制器就前面安裝認(rèn)證模塊生成的。
public function auth(array $options = [])
{
// Authentication Routes...
$this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
$this->post('login', 'Auth\LoginController@login');
$this->post('logout', 'Auth\LoginController@logout')->name('logout');
// Registration Routes...
if ($options['register'] ?? true) {
$this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
$this->post('register', 'Auth\RegisterController@register');
}
// Password Reset Routes...
if ($options['reset'] ?? true) $this->resetPassword();
// Email Verification Routes...
if ($options['verify'] ?? false) $this->emailVerification();
}
public function resetPassword()
{
$this->get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
$this->post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
$this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
$this->post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');
}
public function emailVerification()
{
$this->get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
$this->get('email/verify/{id}', 'Auth\VerificationController@verify')->name('verification.verify');
$this->get('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');
}
來看第一條路由,是一條命名路由 name()
,執(zhí)行控制器的方法是 LoginController->showLoginForm()
,這個(gè)方法會(huì)調(diào)出視圖 view('auth.login')
,需要自己建立視圖文件 \resources\views\auth\login.blade.php
,可以使用命令 php artisan make:auth
來生成。登錄表單大概是這樣,@csrf
這是默認(rèn)需要添加的參數(shù),除非在 VerifyCsrfToken.php
中間關(guān)閉了校驗(yàn)。
<style>
.frame { width:50%; margin:auto; padding:32px; background: #484848; border-radius: 4px; color:white; }
.filed { padding:16px; border:1px solid #4E6DB0; border-radius: 16px; background: #282828; margin:16px;}
.center { text-align: center; }
</style>
<div class="frame">
<form method="post">
{{ csrf_field() }}
<!-- @csrf -->
<div class="filed">{{ __('E-Mail Address') }}: <input type="text" name="email"></div>
<div class="filed">{{ __('Password') }}: <input type="password" name="password"></div>
<div class="filed center">
<button type="submit">{{ __('Submit') }}</button>
<input type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
<label class="form-check-label" for="remember">{{ __('Remember Me') }}</label>
</div>
</form>
</div>
所有post請(qǐng)求中必須包含一個(gè) @crsf
的字段用以防止跨域攻擊,只有通過驗(yàn)證才認(rèn)為是安全的提交動(dòng)作,否則會(huì)得到 419 Page Expired。打開 app\Http\Middleware\VerifyCsrfToken.php
添加排除規(guī)則即可關(guān)閉:
protected $except = [
'login',
];
只是,LoginController
類并沒有定義這個(gè)方法,這個(gè)方法定義在 AuthenticatesUsers
中定義的,通過 use
關(guān)鍵字引入 trait Authenticatable,這相當(dāng)于 PHP 的多繼承用法。
use AuthenticatesUsers;
email
和 password
兩個(gè)變量用于匹配用戶,密碼生成使用的是 bcrypt()
方法,自帶的 DatabaseSeeder.php
中含有初始化數(shù)據(jù),按需要去執(zhí)行命令填充到數(shù)據(jù)庫(kù) artisan db:seed
。
還有一些其他的認(rèn)證方法:
Auth::check()
判斷當(dāng)前用戶是否已認(rèn)證(是否已登錄)
Auth::user()
獲取當(dāng)前的認(rèn)證用戶
Auth::id()
獲取當(dāng)前的認(rèn)證用戶的 ID(未登錄情況下會(huì)報(bào)錯(cuò))
Auth::attempt(['email' => $email, 'password' => $password], $remember)
嘗試對(duì)用戶進(jìn)行認(rèn)證
Auth::attempt($credentials, true)
通過傳入 true 值來開啟 '記住我' 功能
Auth::once($credentials)
只針對(duì)一次的請(qǐng)求來認(rèn)證用戶
Auth::login($user)
Auth::login($user, true)
// Login and "remember" the given user...
Auth::login(User::find(1), $remember)
登錄一個(gè)指定用戶到應(yīng)用上
Auth::guard('admin')->login($user)
Auth::loginUsingId(1)
登錄指定用戶 ID 的用戶到應(yīng)用上
Auth::loginUsingId(1, true)
// Login and "remember" the given user...
Auth::logout()
使用戶退出登錄(清除會(huì)話)
Auth::validate($credentials)
驗(yàn)證用戶憑證
Auth::viaRemember()
是否通過記住我登錄
Auth::basic('username')
使用 HTTP Basic Auth 的基本認(rèn)證方式來認(rèn)證
Auth::onceBasic()
執(zhí)行「HTTP Basic」登錄嘗試
Password::remind($credentials, function($message, $user){})
發(fā)送密碼重置提示給用戶
Adding Custom Guards
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
{
/**
* Register any application authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Auth::extend('jwt', function ($app, $name, array $config) {
// Return an instance of Illuminate\Contracts\Auth\Guard...
return new JwtGuard(Auth::createUserProvider($config['provider']));
});
}
}
在 /config/auth.php
中配置自定義的 Guards:
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
Closure Request Guards
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();
});
}
在 /config/auth.php
中配置自定義的 Guards:
'guards' => [
'api' => [
'driver' => 'custom-token',
],
],
Adding Custom User Providers
如果不使用傳統(tǒng)的關(guān)系數(shù)據(jù)庫(kù),可以自定義 Provider 可以實(shí)現(xiàn)自己的認(rèn)證邏輯,如實(shí)現(xiàn)一個(gè) riak Provider:
namespace App\Providers;
use Illuminate\Support\Facades\Auth;
use App\Extensions\RiakUserProvider;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* Register any application authentication / authorization services.
*
* @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'));
});
}
}
在 /config/auth.php
中配置自定義的 riak:
'providers' => [
'users' => [
'driver' => 'riak',
],
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
The User Provider Contract
Illuminate\Contracts\Auth\UserProvider
contract 接口定義:
namespace Illuminate\Contracts\Auth;
interface UserProvider {
public function retrieveById($identifier);
public function retrieveByToken($identifier, $token);
public function retrieveByCredentials(array $credentials);
public function updateRememberToken(Authenticatable $user, $token);
public function validateCredentials(Authenticatable $user, array $credentials);
}
retrieveById()
, retrieveByToken()
, and retrieveByCredentials()
方法返回的對(duì)象需要實(shí)現(xiàn) Authenticatable 接口。
The Authenticatable Contract
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();
}
Events 認(rèn)證事件
認(rèn)證過程中(包括注冊(cè)、忘記密碼),定義了相關(guān)事件,實(shí)現(xiàn)自己的 EventServiceProvider
進(jìn)行監(jiān)聽:
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', ],
];