どうもこんにちは
先日の投稿で、メール認証をやってみましたが、
今回はTwilioを使ったSMS認証をやってみます。
Twilioのチュートリアルに、しっかりやり方書いてあるので、
そっち見てもらったほうが早いかもしれませんが、
私がやったことたちを備忘録として。
ここを参考にしました。 ⇒ Account Verification with Authy, PHP and Laravel – Twilio
目次
目標
こんな感じで行きます。ボタンの適当な英語はご愛嬌。
流れ
・ユーザ登録画面
⇒ これ、Laravelのmake:authで作られるアレをそのまま
⇒ 登録できたら電話番号入力へ
・電話番号入力画面
⇒ 電話番号を入力して「Send SMS」を押すとSMSを送信
・トークン入力画面
⇒ トークンを入力して「Verify」を押すと認証する
⇒ 「Resend SMS」を押すと電話番号入力に戻る
⇒ 「Verifiing Via Phone」を押すと入力していた電話番号に電話がかかってくる
その他
・SMS認証が終わってないとアクセスできないようにmiddlewareを作る。
⇒リダイレクト先はトークン入力画面
では、行きましょう。
Authy\AuthyApiのインストールおよび設定
なにはなくとも、コレです。
Composer
コマンドを実行します。
composer require authy/php
.envを修正
Twilioで作るAuthy PRODUCTION API KEY が必要です。
作り方は公式ドキュメントにお任せです。
AUTHY_API_KEY=[Your Authy PRODUCTION API KEY]
サービスプロバイダに登録
先ほどインストールと設定したAuthy APIをアプリケーション全体で利用できるように
サービスプロバイダに登録します。
app/Providers/AuthyApiProvider.php
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 |
<?php namespace App\Providers; use Authy\AuthyApi; use Illuminate\Support\ServiceProvider; class AuthyApiProvider extends ServiceProvider { /** * Register services. * * @return void */ public function register() { $this->app->singleton(AuthyApi::class, function ($app) { $authyKey = getenv('AUTHY_API_KEY') or die( "You must specify your api key for Authy. " . "Visit https://dashboard.authy.com/" ); return new AuthyApi($authyKey); }); } /** * Bootstrap services. * * @return void */ public function boot() { // } } |
config/app.php
1 2 3 4 |
'providers' => [ // 途中は省略 \App\Providers\AuthyApiProvider::class, // <- 追記ね ], |
データベースの変更
今回はUsersテーブルを直接まさぐりました。
php artisan make:migration prepare_users_table_for_sms_verify --table=users
migrationファイルはこんな感じです。
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 |
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class PrepareUsersTableForSmsVerify extends Migration { /** * Run the migrations. * * @return void */ public function up() { // Schema::table('users', function (Blueprint $table) { $table->string('phone_number')->nullable()->comment('電話番号'); $table->string('country_code')->nullable()->comment('国番号'); $table->string('authy_id')->nullable()->comment('Authy ID'); $table->boolean('sms_verified')->default(false)->comment('SMS認証済み'); }); } /** * Reverse the migrations. * * @return void */ public function down() { // Schema::table('users', function (Blueprint $table) { $table->dropColumn('phone_number'); $table->dropColumn('country_code'); $table->dropColumn('authy_id'); $table->dropColumn('sms_verified'); }); } } |
忘れずに、migrateします。
php artisan migrate
テンプレートファイルの作成
今回は、国番号を81(日本)を固定しています。
必要に応じて、入力になってもよいですね。
デザインとか、そんなちっさいことは気にするな!
それわかちこわかちこー
resources/views/auth/sms/send.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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">{{ __('Phone Number') }}</div> <div class="card-body"> <form method="POST" action="{{ route('smsverification.send') }}"> @csrf <div class="form-group row"> <label for="phone_number" class="col-md-4 col-form-label text-md-right">{{ __('Phone Number') }}</label> <div class="col-md-6"> <input id="phone_number" type="text" class="form-control{{ $errors->has('phone_number') ? ' is-invalid' : '' }}" name="phone_number" value="{{ old('phone_number') }}" required autofocus> @if ($errors->has('phone_number')) <span class="invalid-feedback" role="alert"> <strong>{{ $errors->first('phone_number') }}</strong> </span> @endif </div> {{--国番号は日本固定--}} <input type="hidden" id="country_code" name="country_code" value="81" /> </div> <div class="form-group row mb-0"> <div class="col-md-6 offset-md-4"> <button type="submit" class="btn btn-primary"> {{ __('Send SMS') }} </button> </div> </div> </form> </div> </div> </div> </div> </div> @endsection |
resources/views/auth/sms/verify.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 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 60 |
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">{{ __('Verify') }}</div> <div class="card-body"> <form method="POST" action="{{ route('smsverification.verify') }}"> @csrf <div class="form-group row"> <label for="phone_number" class="col-md-4 col-form-label text-md-right">{{ __('Token') }}</label> <div class="col-md-6"> <input id="token" type="text" class="form-control{{ $errors->has('token') ? ' is-invalid' : '' }}" name="token" value="{{ old('token') }}" required autofocus> @if ($errors->has('token')) <span class="invalid-feedback" role="alert"> <strong>{{ $errors->first('token') }}</strong> </span> @endif </div> </div> <div class="form-group row mb-0"> <div class="col-md-6 offset-md-4"> <button type="submit" class="btn btn-primary"> {{ __('Verify') }} </button> </div> </div> </form> </div> <div class="card-body"> <div class="form-group row mb-0"> <div class="col-md-6 offset-md-4"> <form action="{{ route('smsverification.send') }}"> <button class="btn btn-primary"> {{ __('Resend SMS') }} </button> </form> </div> <div class="col-md-6 offset-md-4"> <form method="POST" action="{{ route('smsverification.call') }}"> @csrf <button class="btn btn-primary"> {{ __('Verifiing Via Phone') }} </button> </form> </div> </div> </div> </div> </div> </div> </div> @endsection |
コントローラ作成
あまり難しく考えてません。サンプルのコードを見ながら、合うようにちょこちょこ直しただけです。
特筆すべきは、
・各アクションメソッドの引数に、サービスプロバイダに登録したAuthyApiオブジェクトが登場していること
⇒本当は必要なところだけでよいのですが、勢いで。
・71行目「$authyApi->requestSms($user->authy_id);」でSMSを送信している。
・126行目「$authyApi->phoneCall($user->authy_id);」で電話をかけている。
といったところでしょうか。
app/Http/COntrollers/Auth/SmsVerificationController.php
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
<?php namespace App\Http\Controllers\Auth; use App\User; use Authy\AuthyApi; use Illuminate\Auth\Events\Verified; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Auth; use Illuminate\Support\MessageBag; class SmsVerificationController extends Controller { /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('auth'); $this->middleware('throttle:6,1')->only('verify', 'send'); } /** * Show the form for sms verification. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function showRegisterPhoneNumberForm(Request $request) { return $request->user()->sms_verified ? redirect()->intended('/') : view('auth.sms.send'); } /** * Show the form for sms verification. * * @param \Illuminate\Http\Request $request * @param AuthyApi $authyApi * @return \Illuminate\Http\Response * * @throws \Illuminate\Validation\ValidationException */ public function send(Request $request, AuthyApi $authyApi) { $this->validate($request, [ 'phone_number' => 'required|numeric', 'country_code' => 'required', ]); $user = Auth::user(); $authyUser = $authyApi->registerUser( $user->email, $request->input('phone_number'), $request->input('country_code') ); if ($authyUser->ok()) { $user->phone_number = $request->input('phone_number'); $user->country_code = $request->input('country_code'); $user->authy_id = $authyUser->id(); $user->save(); $request->session()->flash( 'status', "User created successfully" ); $sms = $authyApi->requestSms($user->authy_id); return redirect()->route('smsverification.verify'); } else { $errors = $this->getAuthyErrors($authyUser->errors()); return view('auth.sms.send', ['errors' => new MessageBag($errors)]); } } /** * @param Request $request * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\View\View */ public function showVerificationForm(Request $request) { return $request->user()->sms_verified ? redirect()->intended('/') : view('auth.sms.verify'); } /** * @param Request $request * @param AuthyApi $authyApi * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\View\View * @throws \Illuminate\Validation\ValidationException */ public function verify(Request $request, AuthyApi $authyApi) { $this->validate($request, [ 'token' => 'required', ]); $user = Auth::user(); $token = $request->input('token'); $verification = $authyApi->verifyToken($user->authy_id, $token); if ($verification->ok()) { $user->sms_verified = true; $user->save(); event(new Verified($request->user())); return redirect()->intended('/'); } else { $errors = $this->getAuthyErrors($verification->errors()); return view('auth.sms.verify', ['errors' => new MessageBag($errors)]); } } /** * @param Request $request * @param AuthyApi $authyApi * * @return \Authy\AuthyResponse */ public function ringPhone(Request $request, AuthyApi $authyApi) { $user = Auth::user(); $response = $authyApi->phoneCall($user->authy_id); if ($response->ok()) { return back(); } else { return view('auth.sms.verify', ['errors' => new MessageBag($this->getAuthyErrors($response->errors()))]); } } /** * @param $authyErrors * @return array */ private function getAuthyErrors($authyErrors) { $errors = []; foreach ($authyErrors as $field => $message) { array_push($errors, $field . ': ' . $message); } return $errors; } } |
あと、登録時に電話番号入力側に流したいので、RegisterControllerにちょいっと追記。
登録後にリダイレクトするだけです。
1 2 3 4 5 6 7 8 9 |
/** * @param Request $request * @param $user * @return \Illuminate\Http\RedirectResponse */ protected function registered(Request $request, $user) { return redirect()->route('smsverification.send'); } |
ルーティング
いつものように、追記です。
routes/web.php
1 2 3 4 5 |
Route::get('sms/send', 'Auth\SmsVerificationController@showRegisterPhoneNumberForm')->name('smsverification.send'); Route::post('sms/send', 'Auth\SmsVerificationController@send'); Route::get('sms/verify', 'Auth\SmsVerificationController@showVerificationForm')->name('smsverification.verify'); Route::post('sms/verify', 'Auth\SmsVerificationController@verify'); Route::post('sms/call', 'Auth\SmsVerificationController@ringPhone')->name('smsverification.call'); |
ミドルウェア
メール認証の時にお世話になったEnsureEmailIsVerifiedを参考に、こんな感じに仕上げました。
認証していないときで、リダイレクト先を与えられなかった時は、
トークン入力画面にリダイレクトさせます。
app/Http/Middlewares/EnsureSmsIsVerified.php
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 |
<?php namespace App\Http\Middleware; use App\User; use Closure; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Redirect; class EnsureSmsIsVerified { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @param string $redirectToRoute * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse */ public function handle($request, Closure $next, $redirectToRoute = null) { if (! $request->user() || ! $request->user()->sms_verified) { return $request->expectsJson() ? abort(403, 'Your email address is not verified.') : Redirect::route($redirectToRoute ?: 'smsverification.verify'); } return $next($request); } } |
あとは、Kernel.phpに登録します。
メール認証のかわりに、こちらを使ってみます。
app/Http/Kernel.php
1 2 3 4 5 6 7 8 9 10 11 12 |
/** * The application's route middleware. * * These middleware may be assigned to groups or used individually. * * @var array */ protected $routeMiddleware = [ // 途中は省略して // 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, // <- コメントアウトして 'verified' => \App\Http\Middleware\EnsureSmsIsVerified::class, // <- こっちにすげかえ ]; |
結果
SMS、電話とも日本語でちゃんとできました。
スバらしい。
メール認証とSMS認証、どっちもできるようにもがんばったらできます。
それでは、また