iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 30
0
Software Development

新手後端工程師的學習歷程系列 第 30

Day 30 - Laravel Authentication - error response 篇

tags: 2019鐵人賽 Laravel Authentication header

前言

Laravel 實作認證功能非常簡單。而且幾乎所有東西都已經幫你設定好了。設定認證系統的檔案在 config/auth.php,其中還包含了幾個好用的選項用來調整認證系統。

我覺得最厲害的地方是 provider 定義如何從資料庫中取得使用者資料。Laravel 內建支援使用 Eloquent 和資料庫查詢產生器來取得使用者資料。

但是方便歸方便,在一開始認證失敗的時候,我們到底怎麼取得錯誤碼,而不是單純的導向新的網頁頁面?

middleware(‘auth’) 是怎麼實作的

透過 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!搞懂流程,就可以來解決它!

不想用 redirectTo() 可以嗎?

思路搞懂了,解決起來就方便了!

來看看 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 格式的錯誤碼

用 Postman 測試看看

第二個方法就是搞定 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。

用 Postman 測試看看


上一篇
Day 29 - Laravel Authentication - Hasher 篇
系列文
新手後端工程師的學習歷程30

尚未有邦友留言

立即登入留言