108
89

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Laravel】API作成時のAuth(認証)機能カスタマイズ例まとめ

Last updated at Posted at 2018-12-19

この記事は

入江開発室アドベンドカレンダー201820日目の記事です。

前回はaoki_tashiproさんの入江開発室 体験談 劇的ビフォーアフターでした。

自己紹介

福岡のFusicというWeb開発企業でエンジニアをしてる吉野です。
普段の業務ではCakePHPやVue.jsをやってます。
福岡でGeek Studioというエンジニア、デザイナー向けのコワーキングスペースを運営してたりもします。

最近案件でLaravelを使う機会があり、「入江開発室」にはLaravelを使う方が多いということもあり、今回Laravelの認証周りの記事を書くことにしました。

php artisan make:auth

Laravelで認証機能を作成する際は


$ php artisan make:auth

このコマンドを叩くだけで、できてしまいます。
マイグレーションファイルまで作ってくれて、ほんと便利なコマンドですね。

ただ、このコマンド、APIを作成する際には色々とカスタマイズをする必要があります。

今回はそのカスタマイズ例を紹介していきます。

前提


$ php artisan make:auth
$ php artisan migrate

この二つのコマンドを実行した後、からスタートします。

JWT認証を導入する

初期設定

今回はJWT認証を用いて、認証をカスタマイズしていきます。

JWT認証では、Laravel側で認証用のトークンを発行し、トークンがヘッダーに含まれているリクエストを認証済みユーザーとして認識する、という仕様になっています。

composerでこちらのプラグインをインストールします。

$ php composer.phar require tymon/jwt-auth 1.0.0-rc3

jwt-authのドキュメントに従って、初期設定をしていきます。


$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
$ php artisan jwt:secret

Userモデルを編集

app/User.php


<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject; // 追加

class User extends Authenticatable implements JWTSubject // 追加
{
    use Notifiable;

    protected $fillable = [
        'username', 'email', 'password'
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

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

    // 追加
    public function getJWTCustomClaims()
    {
        return [];
    }

config/auth.phpを修正

config/auth.php


'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'jwt', // 変更
            'provider' => 'users',
        ],
    ],

routes/api.phpを修正

Route::group(["middleware" => "api"], function () {
// 認証が必要ないメソッド
    Route::group(['middleware' => ['jwt.auth']], function () {
    // 認証が必要なメソッド
    });
});

以上、jwtの初期設定になります。

ユーザーの作成機能(Register)をつくる

app/Http/Controllers/Auth/RegisterController.php にユーザーの作成機能を書いていきます。

// app/Http/Controllers/Auth/RegisterController.php内に追加

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Auth\Events\Registered;

public function register(Request $request): JsonResponse
{
    $validate = $this->validator($request->all());

    if ($validate->fails()) {
        return new JsonResponse($validate->errors());
    }

    event(new Registered($user = $this->create($request->all())));

    return new JsonResponse($user);
}

また、routes/api.phpに以下を追記します。

Route::group(["middleware" => "api"], function () {
    Route::post('/register', 'Auth\RegisterController@register'); // 追加
    Route::group(['middleware' => ['jwt.auth']], function () {
    });
});

これでLaravel側のユーザー作成機能ができました。

curlコマンドで実際に動作確認をします。

$ curl -X POST localhost:8080/api/register -d name=yoshino -d password=yoshino -d password_confirmation=yoshino -d email=sample@example.com
{"name":"yoshino","email":"sample@example.com","updated_at":"2018-12-19 03:34:54","created_at":"2018-12-19 03:34:54","id":1}

作成したユーザーが返ってきます。

ユーザーのログイン機能(Login)をつくる

次にユーザーのログイン機能をつくります。

app/Http/Controllers/Auth/AuthController.php にユーザーの作成機能を書いていきます。

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
// 追加
use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class LoginController extends Controller
{
    use AuthenticatesUsers;

    protected $redirectTo = '/home';
    private $authManager; // 追加

    public function __construct(AuthManager $authManager) // 追加
    {
        $this->authManager = $authManager;  // 追加
        $this->middleware('guest')->except('logout');
    }

    // 追加
    public function login(Request $request): JsonResponse
    {
        $guard = $this->authManager->guard('api');
        $token = $guard->attempt([
            'email' =>  $request->get('email'),
            'password'  =>  $request->get('password'),
        ]);
        if (!$token) {
            return new JsonResponse(__('auth.failed'));
        }
        return new JsonResponse($token);
    }
}

ユーザー作成時と同様に、routesにも追加します。


Route::group(["middleware" => "api"], function () {
    Route::post('/register', 'Auth\RegisterController@register');
    Route::post('/login', 'Auth\LoginController@login'); // 追加
    Route::group(['middleware' => ['jwt.auth']], function () {
    });
});

これでログイン機能ができました。
curlでログインしてみます。

$ curl -X POST localhost:8080/api/login -d email=sample@example.com -d password=yoshino
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODA4MFwvYXBpXC9sb2dpbiIsImlhdCI6MTU0NTE5MjAwOSwiZXhwIjoxNTQ1MTk1NjA5LCJuYmYiOjE1NDUxOTIwMDksImp0aSI6IlR6R01zanB0ODNXam1nanIiLCJzdWIiOjIsInBydiI6Ijg3ZTBhZjFlZjlmZDE1ODEyZmRlYzk3MTUzYTE0ZTBiMDQ3NTQ2YWEifQ.dhXhsCIrmfBuAyVRl1RSUuLJ5PAo9KMPepM5viqjaGw"

jwtトークンが返ってきます。

ログイン済みユーザーだけがアクセスできるページを作成する

ログイン機能を作成したので、ログイン済みユーザーだけがアクセスできるAPIを作ってみましょう。

新しくApiController.phpを作成します。

app/Http/Controller/ApiController.php

<?php

namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;

class ApiController extends Controller
{
    public function index()
    {
        return new JsonResponse('You are authorized user');
    }
}

ただJsonを返すだけのfunctionになります。

次にroutesに追記をします。

Route::group(["middleware" => "api"], function () {
    Route::post('/register', 'Auth\RegisterController@register');
    Route::post('/login', 'Auth\LoginController@login');
    Route::group(['middleware' => ['jwt.auth']], function () {
        Route::get('/home', 'ApiController@index'); // 追記
    });
});

この状態で、curlでアクセスします。

まず、ログインしない状態でアクセスしてみます。

$ curl -X GET localhost:8080/api/home
<!doctype html>
<html lang="en">
    <head>
        <title>Unauthorized</title>

DOMが表示されますが、「Unauthorized」のメッセージが返ってきているのがわかります。

次に先ほどログインした際に取得したトークンを、ヘッダーに入れた状態でアクセスします。

$ curl -X GET localhost:8080/api/home -H 'Authorization:Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODA4MFwvYXBpXC9sb2dpbiIsImlhdCI6MTU0NTIzMTA4MiwiZXhwIjoxNTQ1MjM0NjgyLCJuYmYiOjE1NDUyMzEwODIsImp0aSI6ImhjWDJ4T2FxdFJXNlJoYzYiLCJzdWIiOjIsInBydiI6Ijg3ZTBhZjFlZjlmZDE1ODEyZmRlYzk3MTUzYTE0ZTBiMDQ3NTQ2YWEifQ.zS0T3NqC_UM5xB-L8iN9zYPmbtHhm1C92fYSnj0d6L8'
"You are authorized user"

しっかりアクセスでき、メッセージが返ってきたのがわかります。

これでログイン済みユーザーだけがアクセスできるページが完成しました。

パスワードリセット機能をカスタマイズする

次に、パスワードリセット機能をAPI用にカスタマイズしていきます。

パスワードリセットメールを送信する

まずはメールアドレスを入力すると、指定されたメールアドレスに対し、パスワードリセット用のリンクが送られてくる、ところまでを作っていきます。

app/Http/Controller/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));
    }
}

ここでは4つのfunctionを上書きして、API用に変更しています。

次にパスワードリセットメールを変更する必要があります。
既存のものだと、Laravel側のURLに遷移してしまうためです。

まず、メールの設定をします。

.envの以下の部分にお持ちのsmtpの設定を記述します。

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

次に、フロントエンド側のURLを定義してあげます。

config/frontend.phpを作成します。

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

次に、Userモデルに追記します。

app/User.php


use App\Notifications\CustomPasswordReset; // 追記

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

CustomPasswordResetをつくっていきます。

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
            );
    }
}

最後にメールテンプレートを作成して、routesに追加します。

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(["middleware" => "api"], 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::group(['middleware' => ['jwt.auth']], function () {
        Route::get('/home', 'ApiController@index');
    });
});

これでパスワードリセットメールが送られてきます。

curlで試してみます。(実際試す際は、メールを受け取れるメールアドレスでお試しください)

$ curl -X POST http://localhost:8080/api/password/email -d email=sample@example.com
"Send Reset Mail"

メールを確認すると、以下のようなメールが届きます。

Laravel

Click link below and reset password.
If you did not request a password reset, no further action is required.

リセットパスワード: http://localhost:3000/reset?queryURL=http://localhost:8080/api/password/reset/a0bb4145b83134e86f1fd9824ea5710442a69dc1a4bdfaa23123523cb82b0ec7

これでパスワードリセットメールの受信ができました。

パスワードリセットリンクから、新しいパスワードを入力して、パスワードを更新する

次に送信したリンク先から、パスワードの更新をします。

app/Http/Controller/Auth/ResetPasswordController.phpを編集していきます。

<?php

namespace App\Http\Controllers\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
{
    use ResetsPasswords;
    protected $redirectTo = '/home';

    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',
        ]);
    }
}

これでパスワードのリセットができます。
先ほどメールで送られてきた、リセットリンクのトークンを使って、実際にパスワードリセットをしてみます。

$ curl -X POST http://localhost:8080/api/password/reset/a0bb4145b83134e86f1fd9824ea5710442a69dc1a4bdfaa23123523cb82b0ec7 -d email=yoshino@fusic.co.jp -d password=password -d password_confirmation=password -d token=a0bb4145b83134e86f1fd9824ea5710442a69dc1a4bdfaa23123523cb82b0ec7
"Password Reset"

パスワードがリセットされました。

実際にSPAなどで使用する際は、メールで送られてきたURLのqueryURLに対して、POSTすることによって、パスワードのリセットが可能です。

メール認証機能をカスタマイズする

最後に、メールの認証機能をAPI用にカスタマイズします。

Laravelにはアカウント作成時にメールの認証をするための、インターフェイスが入っています。
しかし、標準のものだと、メールに記載されるリンクがLaravel側のURLであるため、API用に作成してあげる必要があります。

認証メールをカスタマイズ

まずはメール認証の際に送信されるメールをカスタマイズします。

Userモデルを編集します。

app/User.php

<?php

namespace App;

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, MustVerifyEmail // 追加
{
    use Notifiable;

    protected $fillable = [
        'name', 'email', 'password',
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

    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);
    }
}

次に、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);
    }
}

フロントエンドのメール認証用のURLを定義します。

config/frontend.php


<?php
return [
    'url' => env('FRONTEND_URL', 'http://localhost:8888'),
    'reset_pass_url' => env('RESET_PASS_URL', '/reset?queryURL='),
    'email_verify_url' => env('FRONTEND_EMAIL_VERIFY_URL', '/verify?queryURL='), // 追記
];

routesに追記します。

routes/api.php

Route::group(["middleware" => "api"], 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'); // 追加
    Route::group(['middleware' => ['jwt.auth']], function () {
        Route::get('/home', 'ApiController@index');
    });
});

これでユーザー作成時に、認証用のメールが届くようになります。

例のごとく、curlコマンドで試してみます。

$ curl -X POST localhost:8080/api/register -d email=sample@example.com -d name=yoshino -d password=password -d password_confirmation=password
{"name":"yoshino","email":"sample@example.com","updated_at":"2018-12-19 23:12:12","created_at":"2018-12-19 23:12:12","id":4}%

ユーザー作成と同時に、メール認証用のメールがユーザーのメールアドレスあてに送信されます。

送付されたリンクでメールを認証する

次に、送付されたリンクに入り、メールの認証を完了します。

app/Http/Controllers/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');
    }
}

これで、メールで送付されたリンクにアクセスすると、メールの認証が完了します。

curlコマンドで試します。

$ curl -X GET "http://localhost:8080/api/email/verify/6?expires=1545265816&signature=cd0969ca3204bc25ffae1176801d081bc2e3de23dd751f0c28f08df49605d1f5"
"Email Verified"

"Email Verified"の文字が返ってきて、DBを確認すると、Userの「email_verified_at」に日時が登録されます。

こちらも、実際にSPAなどで使用する際には、送られてきたメールのリンクの「queryURL」にGETでアクセスするようにしてください。

まとめ

以上、長くなりましたが、LaravelのAPI用の認証カスタマイズでした。

Laravelは便利な機能がたくさん揃っていますので、なるべく既にある機能をカスタマイズすることで、より効率的な開発を進められるとよいかと思います。

最後まで読んでいただき、ありがとうございました。

108
89
2

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
108
89

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?