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 分为加密和校验两部分。
加密原理
- 按照一定规则对 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 协议》,转载必须注明作者和本文链接