この記事は
入江開発室アドベンドカレンダー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.
これでパスワードリセットメールの受信ができました。
パスワードリセットリンクから、新しいパスワードを入力して、パスワードを更新する
次に送信したリンク先から、パスワードの更新をします。
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は便利な機能がたくさん揃っていますので、なるべく既にある機能をカスタマイズすることで、より効率的な開発を進められるとよいかと思います。
最後まで読んでいただき、ありがとうございました。