用法
簽名 Url 是在命名路由的基礎上新增了簽名(signature)、過期時間(expires)等查詢引數,以滿足需要身份認證的場景。
use Illuminate\Support\Facades\URL;
// 命名路由
route('posts.show', 1);
// /posts/1
// 簽名路由
Url::signedRoute('posts.show' , 1);
// posts/1?signature=048f5d592b51e1025b56be6cf2cd06259915701fba7f16d65c55f4376e963ae6
// 臨時簽名路由
Url::signedRoute('posts.show' , 1, now()->addHour());
// 推薦使用可讀性更高的用法
Url::temporarySignedRoute('posts.show', now()->addHour(), 1);
// posts/1?expires=1578802591&signature=676b89b76a80da01cb45ce9612c6a4da7a9d504f1522e48774c252f7fd092b66
示例
使用者訪問 foo
,返回一個簽名路由的 Url
Route::get('bar', function(){
return '驗證成功';
})->name('bar');
生成訪問的 url
url()->signedRoute('bar');
// http://site.dev/bar?signature=9a88871526fd9033ef31f220457bf3a77604bdf1273e4e2cff87a28344d989d7
訪問該 url ,服務端需要進行簽名的校驗,完善校驗邏輯
Route::get('bar', function(){
if (! request()->hasValidSignature()) {
abort(401);
}
return '驗證成功';
})->name('bar');
也可以使用中介軟體來自動進行校驗
Route::get('bar', function(){
return '驗證成功';
})->name('bar')->middleware('signed');
應用場景示例
簽名 Url 的一個典型的應用場景就是郵件啟用。使用者使用郵箱註冊成功之後,伺服器需要給使用者傳送一個帶連結的郵件,該連線需要具有以下幾個特點:
- 連結是有時效的,過了規定時間連線會時效;
- 該連結需要攜帶認證資訊,以保證是針對特定使用者的啟用連結;
- 該連結無法被隨意篡改(防止其他使用者惡意註冊賬戶)
定義路由
Route::get('email/verify/{id}/{hash}', 'Auth\VerificationController@verify')->name('verification.verify');
生成對應使用者的郵箱啟用連結
URL::temporarySignedRoute(
'verification.verify',
now()->addHour(),
[
'id' => $user->getKey(),
'hash' => sha1($user->email),
]);
// email/verify/1/54bf34d2ea77b4a155c49b898243beabd2c76154?expires=1578803842&signature=4d90aabf6c98b545954f26125bacb4d18b99539e1835ad45b78e7ea35d0327d0
對應郵箱進行校驗
public function verify(Request $request)
{
// 校驗使用者 ID
if (! hash_equals((string) $request->route('id'), (string) $request->user()->getKey())) {
throw new AuthorizationException;
}
// 校驗使用者郵箱
if (! hash_equals((string) $request->route('hash'), sha1($request->user()->getEmailForVerification()))) {
throw new AuthorizationException;
}
// 檢驗簽名
if (! $request->hasValidSignature()) {
throw new AuthorizationException;
}
return true;
}
原理
簽名 url 分為加密和校驗兩部分。
加密原理
- 按照一定規則對 url 引數進行排序;
- 根據排序後的引數生成 url;
- 使用 sha256 演算法 url 進行加密,得到
signature
- 重新生成帶有
signature
的 url
定義路由
Route::get('foo', function(){
return '驗證成功';
})->name('foo');
根據路由來生成簽名 url
use Illuminate\Support\Facades\URL;
// 按照一定規則排列 url 引數
$params = ['b=2', 'c=3', 'a=4'];
$hour = now()->addHour();
$paramsWithExpires = $params + ['expires' => $hour->getTimestamp()];
ksort($paramsWithExpires);
// 根據排序後的引數生成 url
$sortedUrl = route('foo', $paramsWithExpires);
// 加密 url,這裡使用 Laravel 的 key 作為金鑰
$key = app('config')['app.key'];
$signature = hash_hmac('sha256', $sortedUrl, $key);
// 重新生成帶有 `signature` 的 url
$url = route('foo', $paramsWithExpires + compact('signature'));
// 可以與 Laravel 生成的 url 進行對比
// Url::temporarySignedRoute('foo', $hour, $params);
解密原理
- 驗證簽名是否正確
- 驗證簽名是否過期
實現
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
Route::get('foo', function(Request $request){
$url = $request->url();
// 獲取不帶簽名的 url
$original = rtrim($url.'?'.Arr::query(
Arr::except($request->query(), 'signature')
), '?');
// 對 url 進行加密,生成簽名
$signature = hash_hmac('sha256', $original, app('config')['app.key']);
// 將服務端生成的簽名與請求引數的簽名進行比較
if(! hash_equals($signature, (string) $request->query('signature', ''))){
return '驗證失敗:簽名錯誤';
}
// 校驗時間是否過期
if($request->query('expires') && now()->getTimestamp() > $request->query('expires') ){
return '驗證失敗:簽名已過期';
}
return '驗證成功';
})->name('foo');
注意:使用 hash_equals
函式來比較字元換,可以保證函式的消耗時間固定,可以用來防止對方的時序攻擊。
本作品採用《CC 協議》,轉載必須註明作者和本文連結