🍣

Laravel Sanctum(Laravel8)とNuxt.jsによるSPA認証

2020/10/15に公開

概要

業務でフロントを分離したくなってLaravelのAPI認証について調べてみると、
Laravel7からSanctumの利用が推奨されていた。
Nuxtとの連携を調査したので作業ログも兼ねて記事として残します。

Sanctumについて

APIトークンを発行するタイプと、SPA認証の2種類を提供するライブラリです。
ここではSPA認証を紹介します。
和訳ドキュメント

完成品

最終的なコードを置いておきます。
https://github.com/nagi125/nuxt_laravel_auth_spa_sample

環境

docker-composeで構成し、nginxでRPしています。

  • Host:Mac
  • Docker
    • nginx:1.19-alpine
    • node:13.8-alpine (Nuxt2.14.x)
    • php:7.4-fpm-alpine (Laravel8.x)

docker-composeは下記の通りです。

version: '3'
services:
  nginx:
    container_name: nginx
    build:
      context: ./.docker/nginx
      dockerfile: Dockerfile
    ports:
      - 80:80
    tty: true
    restart: always
    depends_on:
      - web

  web:
    container_name: web
    build:
      context: ./.docker/web
      dockerfile: Dockerfile
    environment:
      PORT: '3000'
      HOST: '0.0.0.0'
    expose:
      - 3000
    volumes:
      - ./web:/app
    stdin_open: true
    tty: true
    restart: always
    depends_on:
      - api

  api:
    container_name: api
    build:
      context: ./.docker/api
      dockerfile: Dockerfile
    environment:
      LANG: 'ja_JP.UTF-8'
      TZ: 'Asia/Tokyo'
      LOG_CHANNEL: 'stderr'
      DB_CONNECTION: 'pgsql'
      DB_HOST: 'db'
      DB_PORT: '5432'
      DB_DATABASE: 'laravel_development'
      DB_USERNAME: 'docker'
      DB_PASSWORD: 'docker'
      SESSION_DOMAIN: 'localhost'
      SANCTUM_STATEFUL_DOMAINS: 'localhost'
    volumes:
      - ./api:/app
    expose:
      - 9000
    tty: true
    restart: always
    depends_on:
      - db

  db:
    image: postgres:12
    container_name: db
    environment:
      TZ: 'Asia/Tokyo'
      POSTGRES_USER: 'docker'
      POSTGRES_PASSWORD: 'docker'
      POSTGRES_DB: 'laravel_development'
    volumes:
      - db:/var/lib/postgresql/data
    ports:
      - 5432:5432

volumes:
  db:

「nginx」「web」「api」コンテナの細かい構成については下記から確認してください。
https://github.com/nagi125/nuxt_laravel_auth_spa_sample/tree/main/.docker

検証環境構築

完成品をCloneしていただいて、下記のコマンドで準備。

$ docker-compose up

Nuxt側

$ docker-compose exec web yarn install
$ docker-compose exec web yarn dev

Laravel側

$ docker-compose exec api composer install
$ docker-compose exec api cp .env.example .env
$ docker-compose exec api php artisan key:generate 
$ docker-compose exec api php artisan migrate:refresh --seed

以上で、Nuxt側・Laravel側の準備が完了です。

動作確認

http://localhost/login
メールアドレスとpasswordについてはLaravel側のseedで確認してください。
http://localhost/secret
にアクセスできれば認証成功です。

認証設定について

認証設定について何をしたのか記載していきます。

Laravel側

Sanctumの準備

$ docker-compose exec api composer require laravel/sanctum
$ docker-compose exec api php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

Sanctumの設定

api/app/Http/Kernel.phpにClassを追加

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful; // 追加

// ...

    protected $middlewareGroups = [
        'api' => [
            EnsureFrontendRequestsAreStateful::class, // 追加
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

api/config/cors.phpにCORS設定

<?php

return [
    'paths' => ['api/*'], // 変更
    'allowed_methods' => ['*'],
    'allowed_origins' => ['http://localhost:3000'], // 変更
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true, // 変更
];

.envに環境変数をセットする
※サンプルコードではdocker-compose.ymlに環境変数をセットしています。

SESSION_DOMAIN=localhost
SANCTUM_STATEFUL_DOMAINS=localhost

LoginControllerの準備

最低限の動作さえすればいい実装にしています。実際に使う場合はValidation含め作り込んでください。
api/app/Http/Controllers/Api/Auth/LoginController.php

<?php

namespace App\Http\Controllers\Api\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class LoginController extends Controller
{
    /**
     * @Method POST
     * @param  Request  $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function login(Request $request): JsonResponse
    {
        $credentials = $request->validate([
            'email' => 'required|email',
            'password' => 'required'
        ]);

        if (auth()->attempt($credentials)) {
            return response()->json(['message' => 'OK!'], 200);
        }

        return response()->json(['message' => 'ユーザーが見つかりません。'], 422);
    }
}

Routeの設定

認証用のRouteとUser情報取得用のRouteを設定します。
※ Laravel8からControllerの指定の仕方が変わったので注意してください。
api/routes/api.php

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\Auth\LoginController;

Route::prefix('auth')->group(function() {
    Route::post('/login', [LoginController::class, 'login']);
});

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Laravel側は以上で準備完了です。

Nuxt側

Nuxtのプロジェクト作成にはUniversalモードとSPAモードとありますが、SPAモードで作成します。
※1. SSRではstoreの関係上、MiddlewareでAuthの設定が上手く反映されません。
※2. プロジェクト作成時にAxiosとBootStrapを指定しています。

必要ライブラリのインストール

サンプルを動かせばinstallされていますが、作業ログとして記録しておきます。

$ docker-compose exec web yarn add @nuxtjs/auth

Auth設定

web/nuxt.config.js

export default {
  // ....

  modules: [
    'bootstrap-vue/nuxt',
    '@nuxtjs/auth', // 追記
    '@nuxtjs/axios', // 追記
  ],
  axios: {
    baseURL: "http://localhost",
    credentials: true,
  },
  auth: {
    redirect: {
      login: '/login',
      logout: '/',
      callback: false,
      home: '/'
    },
    strategies: {
      local: {
        endpoints: {
          login: { url: '/api/auth/login', method: 'post', propertyName: false },
          user: { url: '/api/user', method: 'get', propertyName: false },
          logout: false
        },
        tokenRequired: false,
        tokenType: false,
      }
    },
    localStorage: false,
  },
  router: {
    middleware: ['auth']
  },
}

Store設定

store利用するためにindex.js用意しろと怒られるので作成します。

$ docker-compose exec web touch store/index.js

Loginページ設定

サンプルコードを見ていただければよいのですが、Loginページだけ記載しておきます。
web/pages/login.vue

<template>
  <div class="container">
    <h1 class="h1 pb-1 border-bottom border-dark">Login</h1>
    <p>ログイン状態: {{ $auth.loggedIn }}</p>
    <div class="row justify-content-md-center">
      <div class="col-6">
        <form name="login" @submit.prevent="login()">
          <div class="form-group">
            <label for="email">Email</label>
            <input type="email" class="form-control" id="email" placeholder="Enter email" v-model="form.email" required />
          </div>
          <div class="form-group">
            <label for="password">Password</label>
            <input type="password" class="form-control" id="password" placeholder="Enter password" v-model="form.password" required />
          </div>
          <button type="submit" class="btn btn-success">Submit</button>
        </form>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        form: {
          email: '',
          password: ''
        },
      };
    },
    mounted() {
      // csrf対策 
      // nginxでRPする場合は/sanctumがapi側を見に行くようにしてください
      this.$axios.get('/sanctum/csrf-cookie');
    },
    methods: {
      async login() {
        try {
          const response = await this.$auth.loginWith('local', { data: this.form });
          console.log(response);

        } catch(error) {
          console.log(error);
        }
      },
    },
  };
</script>

作業は以上となります。

最後に

今回はLaravel Sanctumを利用したSPA認証について書きました。
課題はSSR時にどうするかですね。そちらも解決できたらまた記事を書きたいと思います。

Discussion