
会員認証やSES バウンス周りやったった。1月中旬までの仕事をがしがし終わらせるのだ🐱https://t.co/XgQdAfsHvW
— 優さん🌷個人開発 (@yuu13n6) January 2, 2020
もくじ
私たちが達成すべきゴール
- Laravel → SES → SNS → SQS → Laravel → DB
送信処理
- LaravelからSESを利用してメール送信する
→正常にメール送信出来たらクローズ - SESがバウンスメールを検知してSNSに発火
- SNSからSQSにバウンスメール情報であるJSONを登録
受信処理
- LaravelからSQSをポーリングしてキューが入っていないか確認しつづける
- LaravelからSQSにキューがあることを検知する
- データベースに処理したキューと同一のMessageIdがないか確認する
●データベースに同一のMessageIdが存在しない
→ データベースに登録してキューを削除
●データベースに同一のMessageIdが存在する
→何も処理をせずにキューを削除
// SQSの標準キューは同一のキューを複製して送信する場合があるのが仕様
バックナンバー
- Laravel6 AWS SES連携 + Bounce, Complaint対応 その①
- Laravel6 AWS SES連携 + Bounce, Complaint対応 その②
- Laravel6 AWS SES連携 + Bounce, Complaint対応 その③
関連
SESの設定
東京リージョンにSESはないのでバージニアで設定
https://console.aws.amazon.com/ses/home?region=us-east-1

- Domain: <ドメイン>
- Generate DKIM Settingsにチェック
Verify This Domainをクリックします。

- 1番目のオレンジ枠:Domain Verification Record
- 2番目のオレンジ枠: DKIM Record Set
スクロールで他に複数あるので注意
Route53
アクセスする
https://console.aws.amazon.com/route53/home?region=us-east-1#hosted-zones:

【ホストゾーン】をクリックして、【ホストゾーンの作成】をクリックします。

SPF設定


SPFが登録された。
DKIM

DKIMも同じように登録していきます。

DKIMを3レコード追加しました。
SPFと合わせて設定できました。
バリュードメイン設定

ドメインの管理をRoute53で統一したいので設定します。
NSレコードをメモします。
バリュードメイン管理画面

設定します。
Laravel+SESに必要なライブラリをインストール
これらのライブラリをインストールしていることが前提です😆
aws-sdk-php
$ composer require aws/aws-sdk-php
guzzlehttp
$ composer require guzzlehttp/guzzle
laravel-sqs-fifo-queue
AWS SQSのFIFOを利用するのでlaravel-sqs-fifo-queueが必要
$ composer require shiftonelabs/laravel-sqs-fifo-queue
// 標準のQueueだと必要ないです。
- 2回届いたりバグが怖いのでSQS FIFOで
- FIFOは300/秒の性能
それ以上の性能を求める場合は標準キュー採用を検討
SES操作用キーの取得
グループの作成

【グループ】をクリックして、【新しいグループの作成】をクリックします。

グループ名を”SES-Admin”に設定して、【次のステップ】をクリックします。

検索欄に”SES”を入力して、”AmazonSESFullAccess”にチェックをする。
【次のステップ】をクリックします。

ユーザの作成

【ユーザー】をクリックして、【ユーザーを追加】をクリックする。

- ユーザ名:ses-admin
- アクセスも種類:プログラムによるアクセス
【次のステップ:アクセス権限】をクリックします。

【ユーザをグループに追加】を選択し、”SES-Admin”にチェックを入れて、【次のステップ:タグ】をクリックします。

【次のステップ:確認】をクリックします。

【ユーザの作成】をクリックします。

アクセスキーIDとシークレットアクセスキーをメモしてください。
次に使いますよ!
コンフィグの設定
config/services.php
'ses' => [
- 'key' => env('AWS_ACCESS_KEY_ID'),
- 'secret' => env('AWS_SECRET_ACCESS_KEY'),
- 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
+ 'key' => env('SES_KEY'),
+ 'secret' => env('SES_SECRET'),
+ 'region' => env('SES_REGION'),
],
上記に設定
.env
MAIL_DRIVER=ses MAIL_FROM_ADDRESS=<認証されたメールアドレス> MAIL_FROM_NAME=MyApp // 任意のアプリ名 SES_KEY=<アクセスキーid> SES_SECRET=<シークレットアクセスキー> SES_REGION=us-east-1
設定反映
$ php artisan config:clear $ php artisan config:cache
送信先アドレス登録




認証リンクをクリックします。

検証が成功しました。
これでこのメールアドレスからメール送信ができるようになります。
テスト送信


認証された送信先メール宛に送信テストを行います。

Gmailで受信できたので、【メッセージのソースを表示】をクリックします。

SPFとDKIMを確認してPASSになってればOK。
Laravelからのメール送信
make:mailを利用します。
ActivationMailableクラスの作成
$ php artisan make:mail ActivationMailable --markdown=emails.activation.created
app/Mail.ActivationMailable.php
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use App\Models\Activation;
class ActivationCreated extends Mailable
{
use Queueable, SerializesModels;
protected $activation;
public function __construct(Activation $activation)
{
$this->activation = $activation;
}
/**
* メール認証にて、メール本文に付与する確認用URL情報を設定
*
* @return $this
*/
public function build()
{
$apiUrl = config('app.url');
return $this->markdown('emails.activation.created')
->with([
'url' => $apiUrl."/users/me/verify?code={$this->activation->code}",
'user_name' => $this->activation->user_name
]);;
}
}
@component('mail::message')
@if (!empty($user_name))
{{ $user_name }} さん
@endif
** 以下の認証リンクをクリックしてください。 **
@component('mail::button', ['url' => $url])
メールアドレスを認証する
@endcomponent
@if (!empty($url))
###### 「ログインして本登録を完了する」ボタンをクリックできない場合は、下記のURLをコピーしてWebブラウザに貼り付けてください。
###### {{ $url }}
@endif
---
※もしこのメールに覚えが無い場合は破棄してください。
---
ご利用有難う御座います。<br>
{{ config('app.name') }}
@endcomponent
登録用コントローラ
app/Http/Api/V1_0/RegisterController.php
<?php
namespace App\Http\Controllers\Api\V1_0;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V1_0\RegistUserRequest;
use App\Models\User;
use App\Models\Activation;
use Illuminate\Http\Request;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Mail\ActivationCreated;
use App\Models\UserRole;
use App\Models\Device;
class RegisterController extends Controller
{
private $code;
private $userGuestRoleId;
private $now;
private $activation;
public function __construct()
{
$userRole = new UserRole();
$this->userGuestRoleId = $userRole->getGuestRoleId();
$this->now = Carbon::now()->format('Y-m-d H:i:s');
}
/**
* 登録リクエストを受付
*
* @param App\Http\Requests\Api\V1_0\RegistUserRequest
* @return void
*/
public function register(RegistUserRequest $request): string
{
$this->createActivation($request);
return response()->json([
'message' => config('mail.message.send_verify_mail')
]);
}
/**
* アクティベーションコードを生成して認証コードをメールで送信
*
* @param App\Http\Requests\Api\V1_0\RegistUserRequest
* @return void
*/
private function createActivation(RegistUserRequest $request): void
{
$activation = new Activation;
$activation->user_name = $request->name;
$activation->email = $request->email;
$activation->password = bcrypt($request->password);
$activation->udid = $request->udid;
$activation->device_os = $request->device_os;
$activation->device_token = $request->device_token;
$activation->code = Uuid::uuid4();
$activation->save();
Mail::to($activation->email)->send(new ActivationCreated($activation));
}
/**
* メール認証コードを検証してユーザ情報の登録
*
* @param Illuminate\Http\Request
* @return string
*/
public function verify(Request $request) :string
{
$this->code = $request->code;
// 認証確認
if (!$this->checkCode($this->code)) {
// 認証確認エラー処理
return response()->json(config('error.mailActivationError'));
} else {
// ユーザ情報, デバイス情報の登録
try {
$retries = (int)3; // トランザクションリトライ回数
DB::beginTransaction(function() {}, $retries);
$this->activation = Activation::where('code',$this->code)->first();
$generalRoleId = $this->userGuestRoleId;
$user = new User();
$user->user_name = $this->activation->user_name;
$user->email = $this->activation->email;
$user->password = $this->activation->password;
$user->user_role_id = $generalRoleId;
$user->save();
Activation::where('code', $this->code)->update(['email_verified_at' => Carbon::now()]);
$user_id = $user->user_id;
$udid = $this->activation->udid;
$device_os = $this->activation->device_os;
$device_token = $this->activation->device_token;
Device::create([
'udid' => $udid,
'device_os' => $device_os,
'device_token' => $device_token
]);
$user->devices()->attach(
['user_id' => $user_id],
['udid' => $udid],
['created_at' => $this->now],
['updated_at' => $this->now]
);
DB::commit();
return response()->json(config('mail.message.add_user_success'));
} catch (\Illuminate\Database\QueryException $e) {
// トランザクションでのエラー処理
DB::rollback();
Log::error('WEB /users/me/verify - Class ' . get_class() . ' - PDOException Error. Rollback was executed.' . $e->getMessage());
return response()->json(config('error.databaseTransactionRollback'));
} catch (\Exception $e) {
// その他のエラー処理
DB::rollback();
Log::error('WEB /users/me/verify - Class ' . get_class() . ' - something went wrong elsewhere.' . $e->getMessage());
return response()->json(config('error.databaseSomethingWentWrongError'));
}
}
}
/**
* メール認証コードの検証
*
* 1. 与えられた認証コードがActivations.codeに存在するか?
* 2. users.emailが存在しユーザ登録が既に完了したメールアドレスかどうか?
* 3. 認証コード発行後1日以内に発行された認証コードであるか?
*
* @param string $code - メール認証のURLパラメータから取得する認証コード
* @return boolean
*/
private function checkCode($code): bool
{
$activation = Activation::where('code',$code)->first();
if (!$activation) {
return false;
}
$activation_email = $activation->email;
$latest = Activation::where('email',$activation_email)->orderBy('created_at', 'desc')
->first();
$user = User::where('email',$activation_email)->first();
$activation_created_at = Carbon::parse($activation->created_at);
$expire_at = $activation_created_at->addDay(1);
$now = Carbon::now();
return $code === $latest->code && !$user && $now->lt($expire_at);
}
}
ルーティング
routes/api.php
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::prefix('V1_0')->group(function () {
Route::middleware([
'jwt_check', // JWTトークン検証 - \App\Http\Middleware\CheckToken::class
'jwt_auth', // JWTトークンによる認証を強制 - \Tymon\JWTAuth\Http\Middleware\Authenticate::class
])->group(function(){
Route::post('/refresh-token', 'Api\V1_0\RefreshTokenController@refreshToken');
});
Route::group([
"middleware" => 'guest:api', // 認証不要なAPIとして設定
], function () {
+ Route::post('/users/me', 'Api\V1_0\RegisterController@register');
Route::post('/login', 'Api\V1_0\LoginController@login');
Route::post('/login/guest', 'Api\V1_0\GuestLoginController@login');
});
});
メソッド:POST
- http://localhost/api/V1_0/users/me
JSON
{
"udid": "udid_string1",
"device_os": "ios",
"device_token": "device_token_string",
"name": "yuu",
"email": "<●認証されたメールアドレス>",
"password": "password"
}
Gmailで確認

SES経由であることが確認できました😊
その②へ


“Laravel6 AWS SES連携 + Bounce, Complaint対応 その①”への1件のコメント