2019鐵人賽
Laravel
Authentication header
Laravel 實作認證功能非常簡單。而且幾乎所有東西都已經幫你設定好了。設定認證系統的檔案在 config/auth.php
,其中還包含了幾個好用的選項用來調整認證系統。
我覺得最厲害的地方是 provider 定義如何從資料庫中取得使用者資料。Laravel 內建支援使用 Eloquent 和資料庫查詢產生器來取得使用者資料。
但是方便歸方便,在一開始認證失敗的時候,我們到底怎麼取得錯誤碼,而不是單純的導向新的網頁頁面?
透過 App/Http/Kernel.php
可以知道 auth
的 middleware 是透過 /App/Http/Middleware/Authenticate::class,
這支 class 處理的
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}
接著找到 /App/Http/Middleware/Authenticate
這支檔案後,又發現它是從 Illuminate/Auth/Middleware/Authenticate
extend 而來
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
// 程式碼省略
}
再進一步找到 Illuminate/Auth/Middleware/Authenticate
這支檔案,如下:
<?php
namespace Illuminate\Auth\Middleware;
use Closure;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\Factory as Auth;
class Authenticate
{
protected $auth;
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($request, $guards);
return $next($request);
}
protected function authenticate($request, array $guards)
{
if (empty($guards)) {
$guards = [null];
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}
throw new AuthenticationException(
'Unauthenticated.', $guards, $this->redirectTo($request)
);
}
protected function redirectTo($request)
{
//
}
}
可以知道當 $request
進來時,透過 handle()
捕捉,而 handle()
又調用了 authenticate()
,最終我們找到錯誤碼透過 AuthenticationException
生成
throw new AuthenticationException(
'Unauthenticated.', $guards, $this->redirectTo($request)
);
Laravel 在錯誤碼的處理是利用 Handler
,細節大家可以參考 官方文件 - Error Handling
那麼我們來找找 Handler
是怎麼實作的吧
首先找到 vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php
這支檔案
<?php
namespace Illuminate\Foundation\Exceptions;
use Exception;
use ...省略
class Handler implements ExceptionHandlerContract
{
public function render($request, Exception $e)
{
if (method_exists($e, 'render') && $response = $e->render($request)) {
return Router::toResponse($request, $response);
} elseif ($e instanceof Responsable) {
return $e->toResponse($request);
}
$e = $this->prepareException($e);
if ($e instanceof HttpResponseException) {
return $e->getResponse();
} elseif ($e instanceof AuthenticationException) {
return $this->unauthenticated($request, $e);
} elseif ($e instanceof ValidationException) {
return $this->convertValidationExceptionToResponse($e, $request);
}
return $request->expectsJson()
? $this->prepareJsonResponse($request, $e)
: $this->prepareResponse($request, $e);
}
protected function unauthenticated($request, AuthenticationException $exception)
{
return $request->expectsJson()
? response()->json(['message' => $exception->getMessage()], 401)
: redirect()->guest($exception->redirectTo() ?? route('login'));
}
}
好啦~找到了!是透過 render()
來捕捉錯誤,最終調用 unauthenticated()
這個方法。
OK!搞懂流程,就可以來解決它!
思路搞懂了,解決起來就方便了!
來看看 unauthenticated()
這個方法到底做了什麼
protected function unauthenticated($request, AuthenticationException $exception)
{
return $request->expectsJson()
? response()->json(['message' => $exception->getMessage()], 401)
: redirect()->guest($exception->redirectTo() ?? route('login'));
}
哦?當 $request->expectsJson()
為真的話才回傳 json 格式的 response,如果為假的話就只能導向新網址
所以只要搞定 exceptsJson()
再幹什麼就可以了
找到實作這個方法的檔案在 vendor/laravel/framework/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php
<?php
namespace Illuminate\Http\Concerns;
use Illuminate\Support\Str;
trait InteractsWithContentTypes
{
public function expectsJson()
{
return ($this->ajax() && ! $this->pjax() && $this->acceptsAnyContentType()) || $this->wantsJson();
}
public function wantsJson()
{
$acceptable = $this->getAcceptableContentTypes();
return isset($acceptable[0]) && Str::contains($acceptable[0], ['/json', '+json']);
}
public function acceptsAnyContentType()
{
$acceptable = $this->getAcceptableContentTypes();
return count($acceptable) === 0 || (
isset($acceptable[0]) && ($acceptable[0] === '*/*' || $acceptable[0] === '*'));
}
}
很好,要讓 expectsJson()
為真,有兩個方法:
wantJson()
而 wantJson()
中又去調用 getAcceptableContentTypes()
方法,實作內容如下
PS: 是透過 vendor/symfony/http-foundation/Request.php
找到
public function isXmlHttpRequest()
{
return 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
}
public function getAcceptableContentTypes()
{
if (null !== $this->acceptableContentTypes) {
return $this->acceptableContentTypes;
}
return $this->acceptableContentTypes = array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all());
}
好了,由此可知,只要送出來的 request 中,header 含有 key:Accept, value: 字串含有 /json 或者 +json 的字樣,就可以回傳 json 格式的錯誤碼
ajax()
為 true、 pjax()
為 false從 vendor/laravel/framework/src/Illuminate/Http/Request.php
可以知道到它們的實作方式, pjax()
比較簡單,只要 header 裡面有 X-PJAX 的字串,就認定為 true,所以只要我們 header 沒有就可以。
public function ajax()
{
return $this->isXmlHttpRequest();
}
public function pjax()
{
return $this->headers->get('X-PJAX') == true;
}
ajax()
又呼叫 isXmlHttpRequest()
方法
ps: 是透過 vendor/symfony/http-foundation/Request.php
中找到
public function isXmlHttpRequest()
{
return 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
}
可以得知 header 中要有 key:X-Requested-With, value: XMLHttpRequest 就會回傳 true。