LoginSignup
8
9

More than 3 years have passed since last update.

モダンな技術を全く知らないSIer5年目がWebサービスを作ることになったため0から勉強する ~Laravel jwt-auth + メール認証 + パスワードリセット~

Last updated at Posted at 2019-04-30

はじめに

現在SIer5年目でjavascript(Jqueryのみ)、PHP(フレームワーク無し)を2年ほど、C#(Windowsアプリ)3年ほどやってきました。
色々なご縁があり、個人で最近Webサービスの立ち上げをやることになったのですが何せ本当にWebサービスを立ち上げるための知識がほぼ0に等しいです:sob:

ただ今後のキャリアを考えた時に今のままではいけないと思いチャレンジすることにしました。

まずは最初に技術を習得しないといけないので、学ぶ&アウトプットするために毎回投稿していこうと思います。
今後身についていこうと思ってるのは下記のような技術です。
AWS
Docker
CI/CD環境の構築
Laravel
Nuxt.js
今回はLaravel+Nuxtについて学んでいきます。

今回学ぶこと

Laravel+Nuxtでのメール認証機能とパスワードリセット機能を作成していこうかと思います。
今回はLaravel API側の実装をしていきます

参考サイト

jwt-auth公式サイト

こちらのサイトを参考にさせていただきました

メール認証の概要は個々がわかりやっすかったです。

前提

Laravel 5.8
Nuxt 2.5.4
jwt-auth

ログイン機能などはこちらで実装してるのでこちらを参照ください

最終的なソースコードはこちらになります。

パスワードリセット機能の実装

php artisan make:authで作成されたForgotPasswordControllerをAPI用にカスタマイズしていきます

/app/Http/Controllers/api/Auth/ForgotPasswordController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Password;

class ForgotPasswordController extends Controller
{
    use SendsPasswordResetEmails;

    public function __construct()
    {
        $this->middleware('guest');
    }

    public function sendResetLinkEmail(Request $request)
    {
        //メールリセットのリクエストをチェックする
        $validate = $this->validateEmail($request->all());

        if ($validate->fails()) {
            return new JsonResponse('Email is Invalid');
        }

        $response = $this->broker()->sendResetLink(
            $request->only('email')
        );

        return $response == Password::RESET_LINK_SENT
            ? $this->sendResetLinkResponse($request, $response)
            : $this->sendResetLinkFailedResponse($request, $response);
    }

    //メールアドレスチェック
    protected function validateEmail(array $data)
    {
        return Validator::make($data, [
            'email' => 'required|email',
        ]);
    }

    protected function sendResetLinkResponse(): JsonResponse
    {
        return new JsonResponse('Send Reset Mail');
    }

    protected function sendResetLinkFailedResponse(Request $request, $response): JsonResponse
    {
        return new JsonResponse(trans($response));
    }
}

Nuxt側でのパスワードリセットURLを指定する

/config/frontend.php
<?php
return [
    'url' => env('FRONTEND_URL', 'http://localhost:3000'), //フロントエンドのURL
    'reset_pass_url' => env('RESET_PASS_URL', '/reset?queryURL='), // フロントエンドのパスワードリセットページのURL
];

パスワードリセットのメールを呼び出す関数sendPasswordResetNotificationを追加する

/app/Http/Controllers/Models/User.php
<?php

namespace App\Models;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;
+ use App\Notifications\CustomPasswordReset;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }

+    public function sendPasswordResetNotification($token)
+    {
+        $this->notify(new CustomPasswordReset($token));
+    }
}

パスワードリセットメール通知用のclassを実装する

app/Notifications/CustomPasswordReset.php
<?php

namespace App\Notifications;

use App;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;

class CustomPasswordReset extends Notification
{
    use Queueable;
    public $token;

    public function __construct($token)
    {
        $this->token = $token;
    }

    public function via($notifiable)
    {
        return ['mail'];
    }

    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->subject('パスワードリセット') // 件名
            ->view('emails.resetpass') // メールテンプレートの指定
            ->action('リセットパスワード',
                config('frontend.url') . config('frontend.reset_pass_url') .
                url('api/password/reset', $this->token) //アクセスするURL
            );
    }
}

メールテンプレートの作成を行う

/resources/views/emails/resetpass.blade.php
<h3>
    <a href="{{ config('app.url') }}">{{ config('app.name') }}</a>
</h3>
<p>
    {{ __('Click link below and reset password.') }}<br>
    {{ __('If you did not request a password reset, no further action is required.') }}
</p>
<p>
    {{ $actionText }}: <a href="{{ $actionUrl }}">{{ $actionUrl }}</a>
</p>

ルートの定義を追加する

/routes/api.php
Route::group(['prefix' => '/auth', ['middleware' => 'throttle:20,5']], function (){
   Route::post('/register', 'Auth\RegisterController@register');
   Route::post('/login', 'Auth\LoginController@login');
+   Route::post("/password/email", "Auth\ForgotPasswordController@sendResetLinkEmail"); 
+   Route::post("/password/reset/{token}", "Auth\ResetPasswordController@reset"); 
});

メール送信テスト

今回は、mailtrapというサービスを使用して簡単にローカルでもメール送信テストをしていきたいと思います

ここを参考に構築しました

.env参考サイトを元に記載しました。

MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=XXXXX
MAIL_PASSWORD=XXXXX
MAIL_ENCRYPTION=null

PostManでテストする

image.png

mailtrapでも送信ができていることを確認する

image.png

パスワードリセットするリクエストを実行するAPIを実装

/app/Http/Controller/api/Auth/ResetPasswordController.php
<?php

namespace App\Http\Controllers\api\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
+ use Illuminate\Http\Request;
+ use Illuminate\Support\Str;
+ use Illuminate\Support\Facades\Validator;
+ use Illuminate\Support\Facades\Password;
+ use Illuminate\Auth\Events\PasswordReset;
+ use Illuminate\Contracts\Auth\PasswordBroker;
+ use Illuminate\Http\JsonResponse;

class ResetPasswordController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Password Reset Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling password reset requests
    | and uses a simple trait to include this behavior. You're free to
    | explore this trait and override any methods you wish to tweak.
    |
    */

    use ResetsPasswords;

    /**
     * Where to redirect users after resetting their password.
     *
     * @var string
     */
    protected $redirectTo = '/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest');
    }

+     public function reset(Request $request)
+     {
+         $validate = $this->validator($request->all());
+ 
+         if ($validate->fails()) {
+             return new JsonResponse($validate->errors());
+         }
+         $response = $this->broker()->reset(
+             $this->credentials($request), function ($user, $password) {
+             $this->resetPassword($user, $password);
+         }
+         );

+         return $response == Password::PASSWORD_RESET
+             ? $this->sendResetResponse($request, $response)
+             : $this->sendResetFailedResponse($request, $response);
+     }

+     protected function resetPassword($user, $password)
+     {
+         $user->forceFill([
+             'password' => bcrypt($password),
+             'remember_token' => Str::random(60),
+         ])->save();
+     }

+     protected function sendResetResponse(Request $request, $response)
+     {
+         return new JsonResponse('Password Reset');
+     }

+     protected function sendResetFailedResponse(Request $request, $response)
+     {
+         return new JsonResponse($response);
+     }

+     protected function validator(array $data)
+     {
+         return Validator::make($data, [
+             'token' => 'required',
+             'email' => 'required|email',
+             'password' => 'required|confirmed|min:6',
+         ]);
+     }
+ }

PostManでパスワードが変更できるのか確認する

image.png

メール認証機能の実装

/app/Models/User.php
<?php

namespace App\Models;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;
use App\Notifications\CustomPasswordReset;
+ use App\Notifications\VerifyEmail;


- class User extends Authenticatable implements JWTSubject
+ class User extends Authenticatable implements JWTSubject, MustVerifyEmail
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }

    public function sendPasswordResetNotification($token)
    {
        $this->notify(new CustomPasswordReset($token));
    }

+     public function sendEmailVerificationNotification()
+     {
+         $this->notify(new VerifyEmail);
+     }
}

メール認証のURLを生成するVerifyEmailを作成する

/app/Notifications/VerifyEmail.php
<?php

namespace app\Notifications;

use App;
use Illuminate\Auth\Notifications\VerifyEmail as VerifyEmailBase;
use Illuminate\Support\Facades\URL;
use Carbon\Carbon;

class VerifyEmail extends VerifyEmailBase
{
    protected function verificationUrl($user)
    {
        $prefix = config('frontend.url') .config('frontend.email_verify_url');
        $routeName = 'verification.verify';
        $temporarySignedURL = URL::temporarySignedRoute(
            $routeName, Carbon::now()->addMinutes(60), ['id' => $user->getKey()]
        );

        return $prefix . urlencode($temporarySignedURL);
    }
}

Nuxt側の認証画面URLを定義する

/config/frontend.php
<?php
return [
    'url' => env('FRONTEND_URL', 'http://localhost:3000'), //フロントエンドのURL
    'reset_pass_url' => env('RESET_PASS_URL', '/reset?queryURL='), // フロントエンドのパスワードリセットページのURL
+     'email_verify_url' => env('FRONTEND_EMAIL_VERIFY_URL', '/verify?queryURL='), // メール認証のURL
];

認証APIのルートも定義する

/routes/api.php
Route::group(['prefix' => '/auth', ['middleware' => 'throttle:20,5']], function (){
   Route::post('/register', 'Auth\RegisterController@register');
   Route::post('/login', 'Auth\LoginController@login');
   Route::post("/password/email", "Auth\ForgotPasswordController@sendResetLinkEmail");
   Route::post("/password/reset/{token}", "Auth\ResetPasswordController@reset");
+    Route::get('email/verify/{id}', 'Auth\VerificationController@verify')->name('verification.verify');
+    Route::post('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');
});

ユーザ登録時にメール認証のためのメール送信を行うためにRegisterControllerを修正する

/app/Http/Controllers/Auth/RegisterController.php
- $user = $this->create($request->all());
+ event(new Registered($user = $this->create($request->all())));

ユーザ登録を行い、メールが送られてることを確認する
image.png

認証を完了するAPIを実装する

/app/Http/Controllers/api/Auth/VerificationController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\VerifiesEmails;
use Illuminate\Http\Request;
use Illuminate\Auth\Events\Verified;
use App\User;
use Illuminate\Http\JsonResponse;

class VerificationController extends Controller
{
    use VerifiesEmails;

    public function __construct()
    {
        $this->middleware('throttle:6,1');
    }

    public function verify(Request $request)
    {
        $user = User::find($request->route('id'));
        if (!$user->email_verified_at) {
            $user->markEmailAsVerified();
            event(new Verified($user));
            return new JsonResponse('Email Verified');
        }
        return new JsonResponse('Email Verify Failed');
    }

    public function resend(Request $request)
    {
        $user = User::where('email', $request->get('email'))->get()->first();
        if (!$user) {
            return new JsonResponse('No Such User');
        }
        if ($user->hasVerifiedEmail()) {
            return new JsonResponse('Already Verified User');
        }

        $user->sendEmailVerificationNotification();

        return new JsonResponse('Send Verify Email');
    }
}

認証用のURLを叩いて、DBのemail_verified_atが日時が入っていれば認証完了となる

image.png

まとめ

基本的には、Laravelの機能をAPI用に改造するだけで実装ができてしまうので、本当に便利でした。

次回はNuxtの画面側の実装もしていきたいと思います。

8
9
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
8
9