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

原文

用法

签名 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 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!