Laravel Url 使用指南 4-4 簽名 Url 的使用及原理

心智極客發表於2020-01-12

原文

用法

簽名 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 分為加密和校驗兩部分。

加密原理

  1. 按照一定規則對 url 引數進行排序;
  2. 根據排序後的引數生成 url;
  3. 使用 sha256 演算法 url 進行加密,得到 signature
  4. 重新生成帶有 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);

解密原理

  1. 驗證簽名是否正確
  2. 驗證簽名是否過期

實現

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 協議》,轉載必須註明作者和本文連結

相關文章