由於新專案決定使用 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); }