由於新專案決定使用 Laravel 開發,在實作 Email 驗證時發現與 Devise 不同的是,它沒有在資料表內建立 confirmation_token
欄位,於是便讓我想了解一下它產生驗證網址的方式。
在追蹤原始碼後發現驗證網址是透過 verificationUrl
函數所產生的。
/**
* 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 地址。/** * Get the email address that should be used for verification. * * @return string */ public function getEmailForVerification() { return $this->email; }
接著來看看 temporarySignedRoute
函數又做了哪些事。
/**
* 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
。$url->setKeyResolver(function () { return $this->app->make('config')->get('app.key'); });
行 30 ~ 32:使用傳入的各項參數產生用於驗證的網址,
signature
使用了hash_hmac
函數透過 sha256 演算法搭配$key
將路由編碼,之後便可透過計算signature
來得知網址是否遭到竄改。
以上便是產生驗證網址的大致流程,以下為驗證的原始碼,以興趣的話也可以看看。
/**
* 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);
}