会員認証や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件のコメント