15
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Laravelの署名つきURLってどうやってできてるの?

Last updated at Posted at 2019-06-19

最近、APIサーバで署名付きのURLを生成したいと思う時が多々あるが、何も考えずに一から作るのはしんどかったので、PHPフレームワークのLaravelでLaravel5.6から実装された署名付きURLの実装を見ていきたいと思う

※ 今回の調査に使ったLaravelのバージョンは5.8.17ですが、今後のバージョンアップによって内容が変化する可能性があります。

はじめに

Webアプリケーションを作成していると、一定時間内のみ有効なURLを作成したい場合が多々ある
しかし、署名付きURLの生成方法はあまりQiitaなどでも実装について触れられていないので、実装するとなるとどうやって実装したらいいのか行きつまることも・・・

今回はLaravelの署名付きURLについて調べたので今後の自分の為にも記事にする

調査環境

物理マシーン

  • MacBook (Retina, 12-inch, 2017)
    • CPU: 1.4 GHz Intel Core i7
    • メモリ: 16 G
    • MacOS Mojave

実行環境

  • Docker for Mac: version 18.09.2
    • php-image: php:7.3.5-cli
      • ext: pdo json pdo_mysql
      • Composer: version 1.8.5
    • db: mariadb:10.3.15

上記の通り構成は非常にシンプルにしている

署名付きURLの生成メソッド

Laravelで署名付きURLを生成するときには以下の2つのスタティックメソッドのどちらかを利用する
これらのメソッドは以下のnamespaceに記述されている
Illuminate\Routing\UrlGenerator;

  • 署名付きで有効期限がないURLの時

URL::signedRoute

PHPDoc.php
@method static string signedRoute(string $name, array $parameters = [], \DateTimeInterface|\DateInterval|int $expiration = null, bool $absolute = true)
  • 署名付きで有効期限のあるURLの時

URL::temporarySignedRoute

PHPDoc.php
@method static string temporarySignedRoute(string $name, \DateTimeInterface|\DateInterval|int $expiration, array $parameters = [], bool $absolute = true)

両メソッドの引数を見ると受け取ってる値は変わらないことに気がつくはずだ
しかし、別々のメソッドになっている
これは、expirationを指定しない場合はデフォルト値nullを指定するようになっており、引数を必要としない為である
よって、基本的にsignedRouteの中身を確認することで署名付きURLの正体を知ることができる

実際に記述部分を見ると以下のソースコードになっている

Illuminate\Routing\UrlGenerator.php
public function signedRoute($name, $parameters = [], $expiration = null, $absolute = true)
    {
        $parameters = $this->formatParameters($parameters);

        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);
    }

1行づつ見ていくことにしよう

1行目

$parameters = $this->formatParameters($parameters);

例: $parameters = ['user' => 1]
Format the array of URL parameters. とPHPDocに書いてあるので、おそらくURLエンコードしていると思われる
とくに記号や全角文字を使っていなければ通さなくても良いと思う

2行目

if ($expiration) {
    $parameters = $parameters + ['expires' => $this->availableAt($expiration)];
}

例: $parameters = ['user' => 1, 'expires' => 1560882044]

この部分は有効期限終了する時間のタイムスタンプをexpiresという添字でparametersに追加している

3行目

ソートしてるだけなので、飛ばす

4行目

$key = call_user_func($this->keyResolver);

例: $key = 'base64:hogehogefugafuga'
環境変数に設定したAPP_KEYが代入されている

5行目

$this->route($name, $parameters + [
            'signature' => hash_hmac('sha256', $this->route($name, $parameters, $absolute), $key),
        ], $absolute);

例: $parameters = ['user' => 1, 'expires' => 1560882044, 'signature' => 'acc5a1b36005eff636c8e6097224ededb6d1b6f9c1cd58953ff905bd09bb86a5'

この部分が今回の最大の肝になるsignatureの生成部分である
結果的にsignatureはhash_hmac関数で生成されている
signatureのパラメータを除いたURLをsha256でハッシュ化した値になっている

※ this->route()の部分は以下のようになっている
基本的には$nameにルーティングテーブルのnameと$parametersにパラメータの配列を渡すとクエリパラメータ付きのURLを取得できる
$absoluteをfalseにするとクエリパラメータ部分のみ取得できる

route($name, $parameters = [], $absolute = true)

署名付きURLの検証用メソッド

もちろん、署名付きURLを生成したということは検証もする必要がある

Laravelで署名付きURLを検証するには以下のメソッドを利用する
こちらのメソッドは以下のnamespaceに同じく記述されている
Illuminate\Routing\UrlGenerator;

  • 検証用のメソッド

URL::hasValidSignature

PHPDoc.php
@method bool hasValidSignature(Request $request, $absolute = true)

こちらのメソッドは非常に簡単な作りになっていて、$requestを引数で渡すとbool値を返してくれる

実際に記述部分を見ると以下のソースコードになっている

Illuminate\Routing\UrlGenerator.php
public function hasValidSignature(Request $request, $absolute = true)
    {
        $url = $absolute ? $request->url() : '/'.$request->path();

        $original = rtrim($url.'?'.Arr::query(
            Arr::except($request->query(), 'signature')
        ), '?');

        $expires = $request->query('expires');

        $signature = hash_hmac('sha256', $original, call_user_func($this->keyResolver));

        return  hash_equals($signature, (string) $request->query('signature', '')) &&
               ! ($expires && Carbon::now()->getTimestamp() > $expires);
    }
```

こちらも1行づつ見ていくことにしよう

### 1行目
```php
$url = $absolute ? $request->url() : '/'.$request->path();
```
例: `$url = 'http://localhost:8000/valid'`
検証用のURLのエンドポイント

### 2行目
```php
$original = rtrim($url.'?'.Arr::query(
    Arr::except($request->query(), 'signature')
), '?');
```
例: `http://localhost:8000/valid?expires=1560882044&user=1`
これは、クエリパラメータからsignatureを除いたURLになっていることがわかる
hash_hmacでハッシュ化した元の値っぽい匂いがする

### 3行目
```php
$expires = $request->query('expires');
```
例: `$expires = '1560882044'`
有効期限をクエリパラメータから取得

### 4行目
```php
$signature = hash_hmac('sha256', $original, call_user_func($this->keyResolver));
```
これは、signedRouteの5行目で作成した$signatureと同じ処理をしていることがわかる

### 5行目
```php
hash_equals($signature, (string) $request->query('signature', '')) &&
               ! ($expires && Carbon::now()->getTimestamp() > $expires);
```
ここでようやく署名の検証ができる
4行目で生成したsignatureは送られてきた$signatureと同じハッシュアルゴリズムとキーを使っているので等しいはずである
hash_equalsを利用して比較を行う
最後に有効期限がきれていないか確認を行う


## 終わりに
今回はLaravel5.6から追加された署名付きURLについてLaravelのソースコードを追っていたが結果的にクエリパラメータをhash化して比較しているだけだったので簡単に自作することができそうだ
このように一般に使われているフレームワークでもしっかりとソースコードを読むと意外と無難な実装がされているのが新鮮だった

## 参考
- [LaravelでURLを取得する全6項目実例](https://blog.capilano-fw.com/?p=2537#URL-7)
- [Laravel URL Generation](https://laravel.com/docs/5.8/urls)
- [laravel GitHub Repository](https://github.com/laravel/laravel)
15
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?