Calvert's murmur

Laravel 如何產生 Email 驗證網址

2020-06-04

約 4275 字 / 需 23 分鐘閱讀

由於新專案決定使用 Laravel 開發,在實作 Email 驗證時發現與 Devise 不同的是,它沒有在資料表內建立 confirmation_token 欄位,於是便讓我想了解一下它產生驗證網址的方式。

在追蹤原始碼後發現驗證網址是透過 verificationUrl 函數所產生的。

vendor/laravel/framework/src/Illuminate/Auth/Notifications/VerifyEmail.php
/** * Get the verification URL for the given notifiable. * * @param mixed $notifiable * @return string */ protected function verificationUrl($notifiable) { return URL::temporarySignedRoute( 'verification.verify', Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)), [ 'id' => $notifiable->getKey(), 'hash' => sha1($notifiable->getEmailForVerification()), ] ); }

由以上程式碼可看到它透過了 UrlGenerator 提供的 temporarySignedRoute 函數來產生暫時性的驗證網址。

傳入的參數分別為:

  • 行 10:路由名稱

    'verification.verify'
  • 行 11:到期時間

    Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60))
  • 行 12 ~ 15:欲編碼的資料

    [
        'id' => $notifiable->getKey(),
        'hash' => sha1($notifiable->getEmailForVerification()),
    ]

    從以下原始碼可得知上方參數中的 getEmailForVerification 函數是用來取得用於驗證的 Email 地址。

    vendor/laravel/framework/src/Illuminate/Auth/MustVerifyEmail.php
    /** * Get the email address that should be used for verification. * * @return string */ public function getEmailForVerification() { return $this->email; }

接著來看看 temporarySignedRoute 函數又做了哪些事。

vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
/** * Create a signed route URL for a named route. * * @param string $name * @param array $parameters * @param \DateTimeInterface|\DateInterval|int|null $expiration * @param bool $absolute * @return string * * @throws \InvalidArgumentException */ public function signedRoute($name, $parameters = [], $expiration = null, $absolute = true) { $parameters = $this->formatParameters($parameters); if (array_key_exists('signature', $parameters)) { throw new InvalidArgumentException( '"Signature" is a reserved parameter when generating signed routes. Please rename your route parameter.' ); } if ($expiration) { $parameters = $parameters + ['expires' => $this->availableAt($expiration)]; } ksort($parameters); $key = call_user_func($this->keyResolver); return $this->route($name, $parameters + [ 'signature' => hash_hmac('sha256', $this->route($name, $parameters, $absolute), $key), ], $absolute); } /** * Create a temporary signed route URL for a named route. * * @param string $name * @param \DateTimeInterface|\DateInterval|int $expiration * @param array $parameters * @param bool $absolute * @return string */ public function temporarySignedRoute($name, $expiration, $parameters = [], $absolute = true) { return $this->signedRoute($name, $parameters, $expiration, $absolute); }
  • 行 46:temporarySignedRoute 函數又將傳入的參數再傳遞給 signedRoute 函數。

  • 行 14:將傳入的 $parameters 透過 formatParameters 函數做個整理,接著行 16 ~ 20 確認 $parameters 內沒有名為 signature 的 key,因為在產生簽名路由時, signature 是保留參數。

  • 行 22 ~ 24:判斷是否有到期時間,若有到期時間則將其放到 $parameters 內。

  • 行 26:將 $parameters 按照 key 排序。

  • 行 28:keyResolver 則是在 RoutingServiceProvider 中設為讀取並回傳 app.key

    vendor/laravel/framework/src/Illuminate/Routing/RoutingServiceProvider.php
    $url->setKeyResolver(function () { return $this->app->make('config')->get('app.key'); });
  • 行 30 ~ 32:使用傳入的各項參數產生用於驗證的網址,signature 使用了 hash_hmac 函數透過 sha256 演算法搭配 $key 將路由編碼,之後便可透過計算 signature 來得知網址是否遭到竄改。

以上便是產生驗證網址的大致流程,以下為驗證的原始碼,以興趣的話也可以看看。

vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
/** * Determine if the given request has a valid signature. * * @param \Illuminate\Http\Request $request * @param bool $absolute * @return bool */ public function hasValidSignature(Request $request, $absolute = true) { return $this->hasCorrectSignature($request, $absolute) && $this->signatureHasNotExpired($request); } /** * Determine if the signature from the given request matches the URL. * * @param \Illuminate\Http\Request $request * @param bool $absolute * @return bool */ public function hasCorrectSignature(Request $request, $absolute = true) { $url = $absolute ? $request->url() : '/'.$request->path(); $original = rtrim($url.'?'.Arr::query( Arr::except($request->query(), 'signature') ), '?'); $signature = hash_hmac('sha256', $original, call_user_func($this->keyResolver)); return hash_equals($signature, (string) $request->query('signature', '')); } /** * Determine if the expires timestamp from the given request is not from the past. * * @param \Illuminate\Http\Request $request * @return bool */ public function signatureHasNotExpired(Request $request) { $expires = $request->query('expires'); return ! ($expires && Carbon::now()->getTimestamp() > $expires); }