こんにちはjunです。laravel をAPIサーバーとして扱いフロントはNuxt.jsのSPAで構築するプロジェクトがありました。認証を実装したり初期のセッティングでそこそこ詰まったのでメモがてら記事にします。
この手の記事にありがちな「開発サーバーまでの段階しかやらない」ではなくSPAをビルドしてDocker上の仮想的に作成したサーバー環境で実際に一つのアプリケーションとして動かすとこまでやってみます。
独立したフロントエンド とバックをどうやってつなげて、どんなサーバー構成にすればいいのか?という視点で見ていただければ幸いです。また私も昨日にようやく自分の頭で纏った程度なので、この手のアプリケーション作成がチョットデキル人はぜひアドバイスやコメントお願いします。
Dockerを用いた環境構築概要から話しています。環境がありLravelとNuxtのAPI連携について知りたい人は **「Larvel にJWT認証機能を追加する」**から読みはじめてください。
使用環境 docker:19.03.13 maxOS Catalina:10.15.5 composer:1.10.6 Node.js:12.19.0 Laravel:6.20 Nuxt.js:2.14.6
私が今回構築するサーバー構成は以下の図の感じです。
webサーバーではバーチャルホスト を使って、80・443などの通常のHTTP通信に対してはNuxt.jsのビルドファイルを返します。そして8000ポートにはLaravelのプロジェクトをおいて、APIサーバーとして使用します。
Nuxt.jsはLaravelから発行されたトークンを用いてAPIにアクセスしてデータを引っ張ります。実際の運用ではドメインが付与されるので、例えばservice.com
として、http(s)://service.com
はNuxt.jsへ、そしてNuxt.jsはhttps://api.service.com
へAPIを飛ばします。
バーチャルホスト の設定をすることで公開側とAPIをconfレベルで分けることができます。つまりDockerのwebサーバーイメージには、ApacheまたはNginxの設定ファイルをボリュームして、それぞれのドキュメントルート を指定できるような環境があれば大丈夫です。(無理に私のイメージとかに合わせなくても大丈夫ということです。)
こちら記事で作成したイメージを使用します。cenotsから構築したLAMP環境です。centos_apache
イメージを用いてweb側をビルドしています。
version: '3'
services:
web_1:
image: centos_apache:1.0
depends_on:
- db
volumes:
- ./html/:/var/www/html/
- ./web_1/httpd.conf:/etc/httpd/conf/httpd.conf
- /sys/fs/cgroup:/sys/fs/cgroup:ro
ports:
- "9000:80"
- "3000:3000"
- "8000:8000"
privileged: true
command: /sbin/init
db:
image: mysql:5.7
environment:
MYSQL_DATABASE: trend_system
MYSQL_USER: trend
MYSQL_PASSWORD: trendtrend
MYSQL_ROOT_PASSWORD: rootroot
ports:
- "3306:3306"
volumes:
- laravel_data:/var/lib/mysql
volumes:
laravel_data: {}
ホストマシンのlocalhost:9000
にアクセスするとコンテナのlocalhost:80
につながります。Nuxt.jsの開発サーバーポートとなる3000
はホストとコンテナ一緒にしています。多分使わないのですが php artisan serve
で立てるLaravel開発サーバーポート8000
も一応用意しておきます。
ホスト側にはhtml
ディレクトリにfrontend
、laravel
と言ったディレクトリがあります。Nuxt、Laravelのソースがそれぞれに配置されています。コンテナ起動時にはそのhtml
ディレクトリはコンテナの/var/www/html
にボリュームされます。
Nginx兄貴達にはすみませんが、とりあえず以下のようなバーチャルホスト 設定が立てられば大丈夫です。
Listen 8000
<Directory "/var/www/html/frontend/dist">
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/html/frontend/dist
</VirtualHost>
<VirtualHost *:8000>
ServerName example.com
DocumentRoot /var/www/html/laravel/public
</VirtualHost>
forntendはnuxtのプロジェクトディレクトリ、laravelはLaravelのプロジェクトディレクトリ名です。80でリクエストが来たらnuxtのビルドしたindex.html
がある/var/www/html/frontend/dist
へ飛ばされます。一応後でブラウザからはアクセスできないようにしますが、8000できたリクエストは/var/www/html/laravel/public
のindex.php
がキャッチします。
初心者の人は「?」となるかもしれません。しかしDBがあり、ホストマシン上でnuxtプロジェクトとLaravelお互いの開発サーバ同士で連絡が取れれば、見出しの内容は実装可能です。ビルドまでしたり総合的に確かめたい場合にDockerを使用してください。
docker-compose.ymlがあるディレクトリでdocker-compose up -d
をします。特に問題なければコンテナーが起動するはずです。起動したら
docker exec -it {web側のコンテナ名} /bin/bash
をしてコンテナに入りましょう。/var/www/html
にいくとマウントしたLaravelプロジェクトとNuxt.jsプロジェクトがいるはずです。
マイグレーションをしたいのでlaravelのenvを設定します。
DB_CONNECTION=mysql
DB_HOST=db // docker-compose.yml で定義したDBのコンテナ
DB_PORT=3306
DB_DATABASE=laravel_test
DB_USERNAME=root
DB_PASSWORD=rootroot
docker-composeのおかげでこの設定であればつながります。コンテナの中/var/www/htmm/laravel
にてマイグレーションを実行します。
$ php artisan migrate
ここからが本番です。ちなみに私が使用しているLaravel はバージョン6.2.0です。バージョンによって設定が異なったりします。この 記事は2020年12月時点でLravel 8 は出ているけど、LTSなどの都合でLravel 6を使用するという状況で書いています。 sライブラリなどはあえてバージョンを指定したりしていますので注意してください。
Laravelには標準でJTWが入っていないのでライブラリをインストールします。
composer require tymon/jwt-auth:1.0.0-rc.5
tymon/jwt-auth
というライブラリを入れますが、Laravel 6 の場合はバージョンに:1.0.0-rc.5
を指定しないとエラーになります。以下はこのライブラリのドキュメント通りの実装です。
ライブラリのインストールをすればJWTは使えるようになります。しかし今回の構築では一部変更しなければならない箇所があったので、設定ファイルを生成します。
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
config/jwt.php
というファイルが生成されたはずです。このファイルにおいて以下の部分を書き換えます。
'providers' => [
/*
|--------------------------------------------------------------------------
| JWT Provider
|--------------------------------------------------------------------------
|
| Specify the provider that is used to create and decode the tokens.
|
*/
// 'jwt' => Tymon\JWTAuth\Providers\JWT\Lcobucci::class,
'jwt' => Tymon\JWTAuth\Providers\JWT\Namshi::class,
]
JWTのプロバイダーをTymon\JWTAuth\Providers\JWT\Namshi::class
に変更します。というのも標準の設定でいくと
Tymon\JWTAuth\Exceptions\JWTException: Could not create token: Implicit conversion of keys from strings is deprecated. Please use InMemory or LocalFileReference classes.
というエラーが生じます。JWTトークンを生成する処理に非推奨な部分があるそうで、プロバイダを変更する必要がありました。参考記事にあるgithub issueがお世話になりました。
JWTトークンを生成するためのシークレットを作成します。
php artisan jwt:secret
するとenvファイルの下の方に
JWT_SECRET=ve1b******
というものが生成されます。ちなみにこのシークレットは絶対に外部に漏れてはいけません。間違ってgithubとかに上げないように .env
ファイルはgitignoreしましょう。
<?php
namespace App;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tymon\JWTAuth\Contracts\JWTSubject; //追加
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 [];
}
}
config/auth.php
にてAuthentication
認証ドライバを変更します。
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => 'api', //変更
'passwords' => 'users',
],
...
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt', //変更
'provider' => 'users',
'hash' => false,
configはキャッシュされていますので、configをいじった後はキャッシュクリアをしましょう。
php artisan config:clear
認証のルートを作成します。route/api.php
に以下のように記述します。
Route::prefix('v1')->group(function(){
Route::group(['middleware' => 'api', 'prefix' => 'auth'], function ($router) {
Route::post('login', 'AuthController@login')->name('login');;
Route::post('logout', 'AuthController@logout');
Route::post('refresh', 'AuthController@refresh');
Route::get('me', 'AuthController@me');
});
});
ルートの設定は運用によって変わると思いますが、APIはバージョンで予め分けておくと将来の拡張性が高まります。一回ぽっきりのサービスでも実装して損はないと思います。上記のルートでは以下のような設定になります。
$ php artisan route:list
+--------+----------+---------------------+-------+---------------------------------------------+--------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+----------+---------------------+-------+---------------------------------------------+--------------+
| | POST | api/v1/auth/login | login | App\Http\Controllers\AuthController@login | api |
| | POST | api/v1/auth/logout | | App\Http\Controllers\AuthController@logout | api,auth:api |
| | GET|HEAD | api/v1/auth/me | | App\Http\Controllers\AuthController@me | api,auth:api |
| | POST | api/v1/auth/refresh | | App\Http\Controllers\AuthController@refresh | api,auth:api |
+--------+----------+---------------------+-------+---------------------------------------------+--------------+
それぞれのルートはAuthController.php
につなげる予定ですので、そのコントローラーを作成します。
php artisan make:controller AuthController
AuthController.php
は以下のような設定になります。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthController extends Controller
{
/**
* Create a new AuthController instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth:api', ['except' => ['login']]);
}
/**
* Get a JWT via given credentials.
*
* @return \Illuminate\Http\JsonResponse
*/
public function login()
{
$credentials = request(['email', 'password']);
if (! $token = auth()->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $this->respondWithToken($token);
}
/**
* Get the authenticated User.
*
* @return \Illuminate\Http\JsonResponse
*/
public function me()
{
return response()->json(auth()->user());
}
/**
* Log the user out (Invalidate the token).
*
* @return \Illuminate\Http\JsonResponse
*/
public function logout()
{
auth()->logout();
return response()->json(['message' => 'Successfully logged out']);
}
/**
* Refresh a token.
*
* @return \Illuminate\Http\JsonResponse
*/
public function refresh()
{
return $this->respondWithToken(auth()->refresh());
}
/**
* Get the token array structure.
*
* @param string $token
*
* @return \Illuminate\Http\JsonResponse
*/
protected function respondWithToken($token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60
]);
}
}
これでJWT認証に必要なLarvel側の設定が終了しました。
きちんとAPIが存在しているかを確かめるために、chrome拡張のTalend API Testerを用いてチェックします。予めシーダーで作成した仮ユーザーデータを用いて以下のように入力してみます。
JSONでアクセストークン が帰ってきました。このアクセストークン をリクエストヘッダに入れてリクエストすることで認証が必要なルートにアクセスできるようになります。試しに /api/v1/auth/me
という現在のユーザー情報を確かめるルートにアクセスしてみます。
HEADERSにAuthorizationというものを追加し、先ほどのトークンを入れています。トークン値は「bearer トークン」という形です。
ユーザー情報が返ってきましたね。Authcontroller@me
で定義した返り値がきちんとえられました。ちなみに認証情報がない場合はHTTP 401を返します。
Nuxt.jsの構築に移る前にCORS対策をします。APIサーバーはブラウザからのアクセスを禁止して、XMLHttpRequest(XHR)のみのリクエストを許可するようにします。そしてXHRにはCORSという制約があります。これがあるとローカル以外からのXHRを用いたAPIアクセスが制限されます。特にNuxtを3000ポートで開発している時でもAPIサーバーと通信できるように準備しておきます。
composer require fruitcake/laravel-cors
protected $middleware = [
// ...
\Fruitcake\Cors\HandleCors::class,
];
php artisan vendor:publish --tag="cors"
先ほどのjwt.php
のようにcors.php
がconfig/
配下に出現します。そのcors.php
を以下のように変更します。
...
/*
* You can enable CORS for 1 or multiple paths.
* Example: ['api/*']
*/
'paths' => ['api/*'],
...
/*
* Matches the request origin. `['*']` allows all origins. Wildcards can be used, eg `*.mydomain.com`
*/
'allowed_origins' => [
'http://localhost',
'http://localhost:3000',// nuxt
'http://localhost:9000' // dockerの9000->80
],
...
api/
ルート配下に対してcors設定を行い、そしてlocalhost
及び3000ポート
からの接続を許可しました。
nuxt.jsの構築は飛ばします。私の環境では nuxt+bootstrap+axiosで始めます。認証系をpluginで構築してもいいのですが、なかなか大変ですのでnuxt authモジュールを使用します。このモジュールがあれば認証つきのNuxtアプリを簡単に作成できます。本家のドキュメントに習いながら進めましょう。
nuxt.jsのプロジェクトルートに移動いして以下のモジュールをインストールします。
npm install @nuxtjs/auth-next @nuxtjs/axios
nuxt.config.js
でまずモジュールの読み込みをします。
...javascript[nuxt.config.js]
// Modules (https://go.nuxtjs.dev/config-modules)
modules: [
'@nuxtjs/axios',
'@nuxtjs/auth'
],
...
これでauthモジュールとaxiosモジュールが使えます。authモジュールはstoreを使用しますので、storeにindex.jsがない場合は空でもいいので作っていきます。
そしてauthモジュールの設定をnuxt.config.js
で行います。
...
auth:{
localStorage: false,
strategies:{
local:{
tokenType:'bearer',
endpoints:{
login:{
url:'/auth/login',
method:'post',
propertyName:'access_token'
},
logout:{
url:'/auth/logout',
method:'post',
},
user:{
url:'/auth/me',
method:'get',
propertyName:false
}
}
},
redirect: {
login: '/login',
logout: '/',
callback: '/login',
home: '/home'
}
}
...
ここでは予めauthモジュールで使用するログイン用のルートを指定したり、使用する通信パターンを定義します。
JWTトークンをローカルストレージに入れておくのは危ないらしいので、localStorage: false
としておきます。そしてstrategies
の中で通信パターンやルートの定義を行います。
今はlocal
という通信パターンしかありませんが、outhとかapi2とか他のapiサーバーに対しての通信パターンを複数定義できます。
tokenType
で先ほどのbeare
を指定しておきます。こうすると自動的にauthorization
ヘッダーにbeare
という文字を追加してくれます。endopoints
でそれぞれのログイン(login)、ログアウト(logout)、ユーザー確認(user)、それぞれのルートを指定します。
指定しないとauthモジュールのデフォルトのURLでアクセスしてしまいます。特にuser
はページが読み込みされた際に、トークンをサーバーに送ってログイン状態かどうかをNuxt.jsに伝える機能があります。ここをキチンと設定しないとNuxt側で現在ログインが必要なページが開けないなどの状態に陥ります。
最後にaxiosの設定をします。
const ENV = require('dotenv').config().parsed;
export default {
...
env:ENV,
axios: {
baseURL: ENV.API_BASE_URL,
},
...
}
nuxtのdotenvモジュールを用いて.env
ファイルからbaseURL
つまりAPIのアクセス先ルートを指定します。.env
は以下のようになっています。
API_BASE_URL=http://localhost:8000/api/v1
今はローカルの開発環境でやっていますが本番はlocalhost
ではなくドメインになったりします。環境ごとに異なる値は.env
に記述して環境ごとに.env
ファイルを作成してそれをインポートします。
デフォルトのページとかを削除してログインフォームとかを用意します。今回はbootstrapで構築しました。このようにヘッダーがあり、「ログイン」をクリックすると入力モーダルが現れます。
この送信をクリックするとnuxt authの関数、loginwith()
が実行されます。コンポーネントレベルですが以下のコードになっています。
<template>
<b-modal
id="login-modal"
ref="modal"
title="ログイン情報を入力してください。"
ok-only
:okTitle="'送信'"
@show="resetModal"
@hidden="resetModal"
@ok="handleOk"
>
<form @submit.stop.prevent="handleSubmit">
<b-alert v-if="loginErrMes" show variant="danger">{{loginErrMes}}</b-alert>
<b-form-group
:state="emailState"
label="メールアドレス"
type="email"
label-for="name-input"
invalid-feedback="メールアドレスは入力必須です。"
>
<b-form-input
id="login-email-input"
v-model="email"
:state="emailState"
required
/>
</b-form-group>
<b-form-group
:state="passState"
label="パスワード"
label-for="name-input"
invalid-feedback="パスワードは入力必須です。"
autocomplete="username"
>
<b-form-input
id="login-password-input"
v-model="pass"
type="password"
:state="passState"
autocomplete="current-password"
required
/>
</b-form-group>
</form>
</b-modal>
</template>
<script>
import { required, minLength, between } from 'vuelidate/lib/validators'
export default {
name:'loginModal',
data(){
return{
email:'',
pass:'',
emailState:null,
passState:null,
loginErrMes:null,
}
},
validations:{
email:{
required,
},
pass:{
required
},
},
methods:{
checkFormHasError(){
this.emailState = !this.$v.email.$invalid;
this.passState = !this.$v.pass.$invalid;
return this.$v.$invalid;
},
resetModal(){
this.email='';
this.pass='';
this.emailState=null;
this.passState=null;
this.loginErrMes=null;
},
handleOk(bvModalEvt){
bvModalEvt.preventDefault()
this.handleSubmit();
},
async handleSubmit() {
if(this.checkFormHasError()) return;
try{
await this.$auth.loginWith('local', { data:{
email:this.email,
password:this.pass
}})
this.resetModal();
this.$store.dispatch('message/setFlashMessage',{
content:'ログインしました。',
messageType:'success'
})
this.$bvModal.hide('login-modal')
}catch(error){
this.loginErrMes='パスワードまたはメールアドレスが異なります。';
}
}
},
}
このアプリでは後でいろいろフォームとかある予定なのでvuelidate
というバリデーションライブラリを入れています。このログインフォーム程度であれば必要ありませんけど。ログイン処理をしているのは下の方にあるasync handleSubmit()
です。入力値が正規値であれば実行されます。
nuxt authはnuxt.config.js
で定義した設定を元にログインのルートにデータをPOSTします。超便利です。
先ほどテストで入れたようにシーダーのアドレスとパスワードを入れて送信します。
ネットワークを見てみるとキチンと8000ポートで待機しているlaravelへ送信されています。レスポンスが200で成功しています。クッキーを見てみるとトークンが保存されているのが分かります。
そしてユーザー情報があるか=ログインしているかで「ログイン」を「ログアウト」を制御しています。
authモジュールを入れている場合は以下のコードでログインしているか、ユーザーの情報を取得することができます。
this.$auth.user
this.$auth.loggedIn
Nuxtは3000ポートの開発サーバで見ていたので、今度はキチンとビルドしてユーザー視点でアクセスしてみましょう。
npm run build
distファイルが出されたのを確認しlocalhost:9000
へアクセスします。私の環境ではホストのlocalhost:9000
はコンテナのlocalhost:80につながります。最初の設定の通り、ドキュメントルート はdist配下に通じているのでindex.htmlが返されます。ログインが成功し、ネットワークでもAPIが送信されているのが分かります。
後はどんどんAPIルートを作成して、nuxtからはaxiosを用いてトークン付きリクエストを送れば認証ルートにアクセスすることができます。これでJWT認証つきのSPAアプリの設定が完了しました。
以上がLravel6とNuxt.jsで構築するJWT認証つきSPAの構築です。サーバーの構成は人によって様々ですがバーチャルホスト で公開側ページとAPIを分けてしまうのが簡単な気がします。とにかくトークン認証を用いたSPAを構築する際には
以上のまとめを意識すればおおよそのNuxt+API認証はわかってくると思います。ReactとかNext.jsなどもこの部分のエッセンスは同じなのでぜひ応用してください。