LoginSignup
6
7

More than 3 years have passed since last update.

Laravel6.x + Vue.js2(TypeScript)のSPAでシンプルなAPI認証を実装する

Last updated at Posted at 2020-05-08

はじめに

最近個人開発したLaravel5.5のアプリケーションを
Laravel6.x + Vue.js2(TypeScript3.8) + Vuetifyでフルリニューアルリリースしました。

LaravelでのSPA認証にハマりどころがあったので、そこを共有してきます。

前提条件

  • Laravel6.xの環境構築は解説しません。Laravel6.xのデフォルト環境があること前提で話を進めます
  • TypeScriptも導入していますが、TypeScriptとVue.jsの環境構築については一旦スルーします
  • 会員登録やログインなどの認証部分のみ画面遷移です

やること一覧

  • api_tokenカラムをusersテーブルに追加する
  • bladeにapi_tokenをセッションに設定する
  • TypeScriptにプラグインを設定し、Authorizationを設定する
  • 設定したプラグインを読み込む
  • RegisterController, LoginControllerにそれぞれapi_tokenをセッションに設定する
  • config/auth.phpのapiのhashをtrueにする
  • Vue.jsから認証情報を取得
  • LaravelでのAPI認証のテスト
  • その他TypeScriptやLaravel-Mixの設定など
  • TypeScript x Vue.jsの型定義ファイル

api_tokenカラムをusersテーブルに追加する

下記コマンドを実行して、マイグレーションファイルを作成する

$ php artisan make:migration add_api_token_to_users_table --table=users
database/migrations/${date}_add_api_token_to_users_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddApiTokenToUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('api_token', 80)->after('password')
                ->unique()
                ->nullable()
                ->default(null);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('api_token');
        });
    }
}
$ php artisan migrate

マイグレーションをお忘れなく。

bladeにapi_tokenをセッションに設定する

resources/views/index.blade.php

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
  <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# website: http://ogp.me/ns/website#">
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <meta name="robots" content="index,follow">

~~ メタ情報省略 ~~

    <meta name="csrf-token" content="{{ csrf_token() }}">

    /** api_tokenを埋め込む **/
    <meta name="api-token" content="{{ session()->get('api_token') ?? '' }}">
    /** ここまで追加 **/

    <title>~~</title>
  </head>
    <body>
      <div id="app" >
        <app />
      </div>
      <script src="{{ mix('/js/app.js') }}"></script>
    </body>
</html>

TypeScriptにプラグインを設定し、Authorizationを設定する

resources/ts/plugins/http.ts
import _Vue from 'vue';
import axios from 'axios';

export default {
  install(Vue: typeof _Vue): void {
    const http = axios.create({
      responseType: "json"
    });

    http.defaults.headers.common['Authorization'] = "Bearer " + document
      .querySelector('meta[name="api-token"]')!
      .getAttribute("content");

    Vue.prototype.$http = http;
  },
};

設定したhttpプラグインを読み込む

resources/ts/app.ts
import Vue from "vue";
import App from "./components/App.vue";
import http from './plugins/http';
Vue.use(http);

new Vue({
  components: {
    app: App
  },
}).$mount('#app')

RegisterController, LoginControllerにそれぞれapi_tokenをセッションに設定する

app/Http/Controllers/Auth/RegisterController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;

class RegisterController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Register Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles the registration of new users as well as their
    | validation and creation. By default this controller uses a trait to
    | provide this functionality without requiring any additional code.
    |
     */

    use RegistersUsers;

    /**
     * Where to redirect users after registration.
     *
     * @var string
     */
    protected $redirectTo = '/';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest');
    }

    /** このメソッドを追加 **/
    protected function registered(Request $request, $user)
    {
        $token = Str::random(80);

        User::where('id', $user->id)
            ->update(['api_token' => hash('sha256', $token)]);

        session()->put('api_token', $token);
    }
    /** ここまで追加 **/

~~ 省略 ~~
}

app/Http/Controllers/Auth/LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
     */

    use AuthenticatesUsers;

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = '/';

    /** このメソッドを追加 **/
    protected function authenticated(Request $request)
    {
        $token = Str::random(80);

        $request->user()->forceFill([
            'api_token' => hash('sha256', $token),
        ])->save();

        session()->put('api_token', $token);
    }
    /** ここまで追加 **/

~~ 省略 ~~
}

config/auth.phpのapiのhashをtrueにする

config/auth.php
<?php

return [

~~ 省略 ~~ 

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

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
            'hash' => true, // ここをtrueにする
        ],
    ],

~~ 省略 ~~ 

Vue.jsから認証情報を取得

resources/ts/components/App.vue
<template>
~~ 省略 ~~
</template>

<script lang="ts">
import { Vue, Component } from "vue-property-decorator";
import { AxiosError, AxiosResponse } from "axios";

@Component
export default class App extends Vue {

  /**
   * created
   */
  public created() {
    this.getUser();
  }

  public getUser() {
    Vue.prototype.$http
      .get(`/api/user`)
      .then((res: AxiosResponse): void => {
        const user = res.data;
        console.log(user)
      })
      .catch((error: AxiosError): void => {
        console.error("APIの認証でエラーが発生しました");
      });
  }
}
</script>

TypeScript(Vue.js含む)をいじったら、コンパイルをお忘れなく

$ npm run dev

認証失敗のログ
スクリーンショット 2020-05-08 13.53.20.png

認証成功のログ
スクリーンショット 2020-05-08 13.55.36.png

LaravelでのAPI認証のテスト

tests/Feature/UserTest.php
<?php

namespace Tests\Feature;

use App\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Str;
use Tests\TestCase;

class UserTest extends TestCase
{

    use DatabaseTransactions;

    /**
     *
     * @return void
     */
    public function test_getユーザー取得()
    {
        $token = Str::random(80);

        $user = factory(User::class)->create([
            'api_token' => hash('sha256', $token),
        ]);

        $response = $this->actingAs($user)->json('GET', "/api/user?api_token={$token}");
        $json = json_decode($response->getContent());

        $this->assertObjectHasAttribute('id', $json);
        $this->assertObjectHasAttribute('name', $json);
        $this->assertObjectHasAttribute('email', $json);
    }

    /**
     *
     *
     * @return void
     */
    public function test_postユーザー取得()
    {
        $token = Str::random(80);

        $user = factory(User::class)->create([
            'api_token' => hash('sha256', $token),
        ]);

        $response = $this
            ->actingAs($user)
            ->withHeaders(
                [
                    'Authorization' => "Bearer ${token}",
                ]
            )
            ->post('/api/user');
        $json = json_decode($response->getContent());

        $this->assertObjectHasAttribute('id', $json);
        $this->assertObjectHasAttribute('name', $json);
        $this->assertObjectHasAttribute('email', $json);
    }
routes/api.php
use Illuminate\Http\Request;

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

良いテスト方法があれば教えてクレメンス。

その他tsconfig.jsonやLaravel-Mixの設定など

tsconfig.json
{
  "compilerOptions": {
    "outDir": "./public/",
    "types": ["vuetify"], // Vuetify入れているので設定しています
    "sourceMap": true,
    "strict": true,
    "noImplicitReturns": true,
    "noImplicitAny": true,
    "module": "es2015",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": "node",
    "target": "es5",
    "lib": [
      "es2016",
      "dom"
    ]
  },
  "include": [
    "./resources/ts/**/*"
  ]
}
webpack.mix.js
const mix = require('laravel-mix');

mix.ts("resources/ts/app.ts", "public/js");

mix.webpackConfig({
  resolve: {
    extensions: [".js", ".jsx", ".vue", ".ts", ".tsx"],
    alias: {
      vue$: "vue/dist/vue.esm.js"
    }
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: "ts-loader",
        options: { appendTsSuffixTo: [/\.vue$/] },
        exclude: /node_modules/
      }
    ]
  }
});

TypeScript x Vue.jsの型定義ファイル

resources/ts/types/index.d.ts
declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}

参考リンク

作成したもの

API関連

TypeScript・Vuetify関連

6
7
0

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
6
7