Webアプリケーションをつくるに当たって、最近はフロントエンドをVueやReactなどでごりごり作りSPAとして運用し、サーバーサイドはAPIサーバーとして別実装して、それぞれ独立して運用するということが一般的になりましたね。
また、サーバーサイドから直接ビューを返すより、APIとしておくほうが、モバイルアプリなども運用したり、別アプリと連携させたりと運用に幅を持たせられます。そのため、APIとして運用していきたいのですが、さて、そのときのログイン認証機構をつくるときはどうするのかということをまとめておきたいと思います。
自身のメモとして作成しておりますので、至らない点多々あるかと思います。また、間違っている部分があれば、私のためにご指摘いただけると幸いです。また、内容を推敲した結果、参考にしたサイトからそのまま引用している箇所が多分にありますので、ご了承ください。
一応自分なりに細かくLaravelの挙動やセキュリティの注意点などもまとめているつもりなので初学者の方はきちんと読んでから実装して欲しいです。慣れている方はむしろ間違いを見つけて指摘してもらえたらありがたいです。
説明抜きで端的に実装する手順については最後にまとめてあります。
参考:公式ドキュメント「Laravel5.8認証」
参考:Laravel + Jwt Auth で認証付きWebAPIを作る
参考:Laravelのmake authで認証機能を作る
環境
2019/07/17執筆時点の環境
- PHP7.3
- Laravel5.8
作るもの
- {ドメイン}/api/login
POSTでユーザーID、パスワードを送信するとトークンが返ってくる - {ドメイン}/api/me
Authorizationヘッダーにトークンをセットしてアクセスするとユーザーの情報を返す
1.認証機能を導入
認証機構をつくる上で気をつかうのがCSRFやSQLインジェクションなどのセキュリティ面の対策です。Laravelにはそれらに必須の対策をよしなに準備してくれる機能が用意されているので、さくっと必要なものを準備します。
(セキュリティについては別途きちんと勉強しましょう)
php artisan make:auth ...①
php artisan migrate ...②
これだけで基本的なログイン機構がつくられています。
①で作られたもの
php artisan make:auth
これによって以下のものが生成されます。
- レイアウトビュー
- resources/views/layouts/app.blade.php
- 登録ログインビュー
- resources/views/auth/login.blade.php
- resources/views/auth/register.blade.php
- resources/views/auth/verify.blade.php
- resources/views/auth/passwords/email.blade.php
- resources/views/auth/passwords/reset.blade.php
- ログイン後のダッシュボード画面
- HomeController
- resources/views/home.blade.php
- すべての認証エンドポイントのルート - routes/api.php(加筆)
- routes/web.php(加筆)
②で行われること
php artisan migrate
database/migrationsの中のmigrationを全て実行します。
デフォルトでは
create_users_table.php
create_password_resets_table.php
の二つが用意されているので、これらのテーブルがDBに作成されます。
※ここでDBの設定がされていないとエラーが出ます。
WebAPIのときに使うのでユーザーを登録しておきましょう。
2.composerでjwt-authをインストール
jwt-authをLaravelプロジェクトにインストールします。
composer require tymon/jwt-auth 1.0.0-rc.4.1
Laravel5.5>= には1.0以上のjwt-authが必要らしいのでバージョン指定します。
指定なしだと0.5くらいのがインストールされようとして、インストール時にエラーはくので注意。
次にjwt-authの初期設定を行います。
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
config/jwt.php
が生成されます。
JWTで用いる秘密鍵を生成。
php artisan jwt:secret
.env
ファイルにJWT_SECRETのパラメータが追加されます。
3.Userモデルを修正
Userモデルをjwt-authに対応させるため、編集します。
<?php
namespace App;
+use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
-class User extends Authenticatable
+class User extends Authenticatable implements JWTSubject
{
...
+ public function getJWTIdentifier()
+ {
+ return $this->getKey();
+ }
+ public function getJWTCustomClaims()
+ {
+ return [];
+ }
}
4.guardを修正
guardは認証を管理する仕組みで、デフォルトではweb
とapi
があります。
web
は普通にhtmlからのログインを管理しています。
一方api
は名前の通り、WebAPIのログインです。
jwt-auth
を使うのでapi
をjwt
に変えます。
config/auth.php
を編集
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
- 'driver' => 'token',
+ 'driver' => 'jwt',
'provider' => 'users',
'hash' => false,
],
],
5.ApiControllerを生成
次にAPIのコントローラーを作成します。
php artisan make:controller ApiController
app/Http/Controllers/ApiController.php
が生成されます。
以下のように変更しましょう。
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
class ApiController extends Controller
{
function login() {
$credentials = request(['email', 'password']);
if (! $token = auth("api")->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $this->respondWithToken($token);
}
public function me()
{
return response()->json(auth()->user());
}
protected function respondWithToken($token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth("api")->factory()->getTTL() * 60
]);
}
}
ログインとユーザー情報を取得するメソッドを作成しました。
6.routes/api.phpを編集
routes/api.php
ファイルを編集。
<?php
use Illuminate\Http\Request;
Route::group(["middleware" => "guest:api"], function () {
Route::post("/login", "ApiController@login");
});
Route::group(["middleware" => "auth:api"], function () {
Route::get("/me", "ApiController@me");
});
/login
はguest
を指定して認証がなくてもアクセスできるように、/me
はログインしてトークンを送らないとアクセスできないようにしました。
7.試す
curl
で試してみます。(ちなみに私はAPIの検証にはPostmanを使っています)
email
とpassword
は上のユーザーを登録したときのを指定します。
curl http://<laravel-host>/api/login -d email=hoge@example.com -d password=hoge12345
すると以下のようにトークンを含んだjsonが返ってきます。
{"access_token":"eyJ0eXAiOiJKV2QiLC2hbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9sYXJhdmVsLXRha2FzYW45ODkuYzl1c2Vygy5pb1wvYXBpXC9sb2dpbiIsImlhdCI6MTUyMjE1NTY1OSwiZXhwIjoxNTIyMTU5MjU5xCJuYmYiOjE1MxIxNTU2NTksImp0aSI6Inh6MHRsQ1hHNmgwQ1g3V0UiLCJzdWIiOjEsInBydiI6Ijg3bTBhZjFlZjlmZDE1ODEyZmorYzk3MTUzYTE0ZTBJfDQ3NTQ2YWEifQ.BdoHaKFy8XLOSaTKBOhA1D3i5NPUGzG9E1lsBQefEhs","token_type":"bearer","expires_in":3600}
そしてトークンを使って認証が必要なurlにアクセスしてみます。
Authorization
ヘッダーにBearer: <token>
を付けて送信します。
curl -H "Authorization: Bearer eyJ0eXAi...." http://<laravel-host>/api/me
実行すると
{"id":1,"name":"hoge","email":"hoge@example.com","created_at":"2018-03-26 14:33:21","updated_at":"2018-03-26 14:33:21"}
ログインしているユーザーの情報が返ってきました。
これを応用すればユーザー認証ができるWebAPIが作れます。
8.ちょっと補足
ちなみに、ちょっとしたことですが、返ってきたデータはuserレコードまんまでないことはお気づきでしょうか?
返すレコードからpasswordとremenber_tokenのカラムだけ削除して返していることに注目してください。これは実はapp/User.php
ファイルの中できちんと隠す項目を設定しているんです。
protected $hidden = [
'password', 'remember_token',
];
ね?
passwordはもちろん、remenber_tokenは、ログイン情報を記憶しておく際に保存するcookie情報の一部となります。
9.まとめ
ここまでの流れを理解した上でさくっと実装するために完結にまとめます
(ここだけ見てやるのは上を理解したうえで行って欲しいな)
1.さくっと認証機構準備
php artisan make:auth ...①
php artisan migrate ...②
2.jwt-authのインストールと初期設定
composer require tymon/jwt-auth 1.0.0-rc.4.1
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secret
3.Userモデルの作成
<?php
namespace App;
+use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
-class User extends Authenticatable
+class User extends Authenticatable implements JWTSubject
{
...
+ public function getJWTIdentifier()
+ {
+ return $this->getKey();
+ }
+ public function getJWTCustomClaims()
+ {
+ return [];
+ }
}
4.guard修正
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
- 'driver' => 'token',
+ 'driver' => 'jwt',
'provider' => 'users',
'hash' => false,
],
],
5.APIコントローラ作成
php artisan make:controller ApiController
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
class ApiController extends Controller
{
function login() {
$credentials = request(['email', 'password']);
if (! $token = auth("api")->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $this->respondWithToken($token);
}
public function me()
{
return response()->json(auth()->user());
}
protected function respondWithToken($token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth("api")->factory()->getTTL() * 60
]);
}
}
6.routes編集
<?php
use Illuminate\Http\Request;
Route::group(["middleware" => "guest:api"], function () {
Route::post("/login", "ApiController@login");
});
Route::group(["middleware" => "auth:api"], function () {
Route::get("/me", "ApiController@me");
});
7.完了!!
というわけで、認証にjwtを用いて自分の情報を取得してくる流れをまとめました。
最後に確認ですが、JWTはトークンの改変を防ぐ(検知する)仕組みを組み込んだトークンです。改変に強い反面、トークン自体を復号することは容易ですので、JWTトークンの中に重要な情報を含まないように気をつけましょう。
※guardの挙動についてまとめきれなかったけど、後日書き足します。