はじめに

Laravel で有効期限付きの一次的な URL を生成してメールで送信する方法について調べた。

基本的には Laravel5.6で署名付きURL(時間制限付き)の実装が簡単に出来るようになったので試した - Qiita の内容通りで、+α として実際にメール送信(承諾/拒否)まで行っている。

TL;DR

目次

  1. はじめに
  2. TL;DR
  3. 環境・条件
  4. 詳細
    1. 前置き
    2. ルーティング
    3. コントローラー
    4. ビュー
    5. リクエスト
    6. メール
      1. HelloMail クラス
      2. 本文
      3. 設定
    7. 動作確認
    8. Tips
  5. まとめ
  6. 参考文献

環境・条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.15.1
BuildVersion: 19B88

$ php -v
PHP 7.3.9 (cli) (built: Sep 10 2019 17:45:01) ( NTS )

$ composer -V
Composer version 1.9.0 2019-08-02 20:55:32

$ composer info laravel/framework
name : laravel/framework
versions : * v6.4.1

詳細

前置き

php artisan って打つのが面倒なので art に省略してる。

1
2
3
4
5
6
7
8
$ type art
art is a function
art ()
{
if [[ -e artisan ]]; then
php artisan $@;
fi
}

ルーティング

routes/web.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php

// 入力フォーム
Route::get('/hello/create', 'HelloController@create')->name('hello.create');
// メール送信
Route::post('/hello', 'HelloController@send')->name('hello.send');
// 承諾リンクをクリック
Route::get('/hello/hi', 'HelloController@hi')->name('hello.hi');
// 拒否リンクをクリック
Route::get('/hello/bye', 'HelloController@bye')->name('hello.bye');
// 期限切れ or 無効URL
Route::get('/hello/invalid', 'HelloController@invalid')->name('hello.invalid');

コントローラー

1
2
$ art make:controller HelloController
Controller created successfully.

有効期限付きの一次的な URL を生成する場合は temporarySignedRoute を使う。

以下を引数として渡す

  1. 対象ルート名称
  2. 有効期限
  3. 追加のパラメータ
    • /user/{id} などに 'id' => 1 を渡したり
    • ルーティングパラメータにない場合はクエリパラメータになったり

また、$request->hasValidSignature() で有効期限切れや、不正 URL ではないかの検証ができる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php

namespace App\Http\Controllers;

use App\Http\Requests\HelloRequest;
use App\Mail\HelloMail;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\URL;

class HelloController extends Controller
{
public function create()
{
return view('hello.create');
}

public function send(HelloRequest $request)
{
$urls = [
'hi' => URL::temporarySignedRoute(
'hello.hi',
now()->addMinutes(1), // 1分間だけ有効
['from' => $request->name]
),
'bye' => URL::temporarySignedRoute(
'hello.bye',
now()->addMinutes(1), // 1分間だけ有効
['from' => $request->name]
),
];
$mail = new HelloMail($request, $urls);
Mail::to($request->email)->send($mail);
return 'sent';
}

public function hi(Request $request)
{
// リンクの検証
if (!$request->hasValidSignature()) {
return redirect()->route('hello.invalid');
}
return 'hi';
}

public function bye(Request $request)
{
// リンクの検証
if (!$request->hasValidSignature()) {
return redirect()->route('hello.invalid');
}
return 'bye';
}

public function invalid()
{
return 'invalid';
}
}

ビュー

入力フォーム(create)は、以下のような最小限の内容で作成。

1
$ touch resources/views/hello/create.blade.php

resources/views/hello/create.blade.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello</title>
</head>
<body>
<form action="{{ route('hello.send') }}" method="POST">
@csrf
<label for="name">Name</label>
<input name="name" type="text" required>
<br>
<label for="name">email</label>
<input name="email" type="email" required>
<br>
<label for="text">text</label>
<input name="text" type="text" required>
<br>
<input type="submit" value="送信">
</form>
</body>
</html>

リクエスト

今回の例だと本筋に関係ないので、使わなくても良いが一応。

1
2
$ art make:request HelloRequest
Request created successfully.

app/Http/Requests/HelloRequest.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class HelloRequest extends FormRequest
{
public function authorize()
{
return true;
}

public function rules()
{
return [
'name' => 'required|string',
'email' => 'required|email',
'text' => 'required|string',
];
}
}

メール

HelloMail クラス

1
2
$ art make:mail HelloMail
Mail created successfully.

app/Mail/HelloMail.php

  • $request, $urls はコントローラから渡される
    • new HelloMail($request, $urls)
  • subject に入力フォームの name を利用
  • ビューファイルで利用するパラメータを with で渡す
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class HelloMail extends Mailable
{
use Queueable, SerializesModels;

public function __construct($request, $urls)
{
$this->request = $request;
$this->urls = $urls;
}

public function build()
{
return $this
->subject($this->request->name.'さんからメールが届きました')
->view('hello.mail.send')
->with([
'urls' => $this->urls,
'text' => $this->request->text,
]);
}
}

本文

メール用のビューファイルを作成

1
2
$ mkdir -p resources/views/hello/mail
$ touch resources/views/hello/mail/send.blade.php

resources/views/hello/mail/send.blade.php

入力フォームのテキストと、承諾/拒否用 の URL のみ

1
2
3
4
5
{{ $text }}
<br>
reply: {{ $urls['hi'] }}
<br>
ignore: {{ $urls['bye'] }}

設定

SMTP とかの設定を .env で実施。Gmail で 2FA 設定してると、アプリパスワードを生成しないといけないので注意

1
2
3
4
5
6
7
8
9
10
11
12
13
-MAIL_DRIVER=smtp
-MAIL_HOST=smtp.mailtrap.io
-MAIL_PORT=2525
-MAIL_USERNAME=null
-MAIL_PASSWORD=null
-MAIL_ENCRYPTION=null
+MAIL_DRIVER=smtp
+MAIL_HOST=smtp.gmail.com
+MAIL_PORT=465
+MAIL_USERNAME=your.mail.account@gmail.com
+MAIL_PASSWORD=your.mail.password
+MAIL_ENCRYPTION=ssl
+MAIL_PRETEND=false

動作確認

以下の内容でメールを送信。

こんなメールが届く。

有効期限内にアクセスすると、それぞれ hibye といった内容が表示される。

有効期限外にアクセスしたり、URL を改変したりすると invalid が表示される。これは $request->hasValidSignature() で検証を行って、検証 NG の場合にはリダイレクトさせているから。

Tips

middleware('signed') を使って、ルーティング設定時に検証対象とすることもできる。

1
2
3
4
Route::group(['middleware' => 'signed'], function() {
Route::get('/hello/hi', 'HelloController@hi')->name('hello.hi');
Route::get('/hello/bye', 'HelloController@bye')->name('hello.bye');
});

ただし、この場合はリダイレクトとかはできない(※)ため、期限切れ URL にアクセスすると(デフォルトだと)こうなる。
※Laravel に詳しくないので本当はできるかも。

まとめ

参考文献

関連記事