10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【備忘録】NuxtとLaravelでログイン機能を実装する 

Last updated at Posted at 2021-02-15

前提

ローカル環境

  • macOS Mojave10.14.6
  • Docker for mac 2.2.0.5

バージョン

  • Laravel...7.30.4
  • @nuxt/types...2.14.7
  • alpine

やりたいこと

  • login_id, passwordでログインする
  • cookieによる認証機能を使う

その他

  • Dockerを使って環境構築済み
  • 自分が作った環境
  • 厳密なエラーハンドリングやバリデーションチェック等は省略

Laravel側の実装

Userモデルの移動関連

/var/www/laravel # mkdir app/Models && \
> mv app/User.php app/Models/
app/Models/User.php
<?php

// App\Modelsに修正
namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{

  // 以下略
database/factories/UserFactory.php
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Models\User;  // ここを修正
use Faker\Generator as Faker;
use Illuminate\Support\Str;

// 以下略
config/auth.php

<?php

return [

  // 中略
  'providers' => [
    'users' => [
      'driver' => 'eloquent',
      'model' => App\Models\User::class,  // ここを修正
    ],
  ],

  // 以下略
Terminal
$ docker-compose exec app ash
$ php artisan -V // 7.30.4
$ composer require laravel/ui:2
$ php artisan ui vue --auth

APIでセッション管理を使った認証機能を有効にする

app/Http/Kernel.php
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{

  (中略)

  protected $middlewareGroups = [
    'web' => [
      \App\Http\Middleware\EncryptCookies::class,
      \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
      \Illuminate\Session\Middleware\StartSession::class,
      // \Illuminate\Session\Middleware\AuthenticateSession::class,
      \Illuminate\View\Middleware\ShareErrorsFromSession::class,
      \App\Http\Middleware\VerifyCsrfToken::class,
      \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],

    'api' => [
+     \App\Http\Middleware\EncryptCookies::class,
+     \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
+     \Illuminate\Session\Middleware\StartSession::class,
+     \Illuminate\View\Middleware\ShareErrorsFromSession::class,
     'throttle:60,1',
      \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
  ];
}

authミドルウェアの設定

config/auth.php
<?php

return [

  (中略)

  'defaults' => [
    'guard' => 'api',  // 'web'から変更
    'passwords' => 'users',
  ],

  (中略)

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

    'api' => [
      'driver' => 'session', // tokenからsessionに変更
      'provider' => 'users',
-      'hash' => false,
    ],
  ],

  (以下略)

];

マイグレーション

database/migrations/2014_10_12_000000_create_users_table.php
<?php

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

class CreateUsersTable extends Migration
{
  /**
   * Run the migrations.
   *
   * @return void
   */
  public function up()
  {
    Schema::create('users', function (Blueprint $table) {
      $table->id();
      $table->string('name');
      $table->string('login_id')->unique(); // 追加
      $table->string('email')->unique();
      $table->timestamp('email_verified_at')->nullable();
      $table->string('password');
      $table->rememberToken();
      $table->timestamps();
    });
  }

  // 以下略
}
$ php artisan migrate

シーディング

$php artisan make:seeder UsersTableSeeder
database/factories/UserFactory.php
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Models\User;
use Faker\Generator as Faker;
use Illuminate\Support\Str;

$factory->define(User::class, function (Faker $faker) {
  return [
    'name' => $faker->name,
    'login_id' => $faker->unique()->userName(), // 追加
    'email' => $faker->unique()->safeEmail,
    'email_verified_at' => now(),
    'password' => \Hash::make('nininopassword'), // passwordから変更
    'remember_token' => Str::random(10),
  ];
});
database/seeds/UsersTableSeeder.php
<?php

use Illuminate\Database\Seeder;
use App\Models\User;

class UsersTableSeeder extends Seeder
{
  /**
   * Run the database seeds.
   *
   * @return void
   */
  public function run()
  {
    \DB::table('users')->truncate();
    factory(User::class, 1)->create([
      'login_id' => 'testuser',
    ]);
  }
}
database/seeds/DatabaseSeeder.php

public function run()
{
  $this->call(UserSeeder::class);
  }
$ php artisan db:seed

ルーティング

routes/api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Auth;  // 追加

// 追加
Route::group(["middleware" => "api"], function () {

  Route::get('/current_user', function () {
    return Auth::user();
  })->name('current_user');

  Route::namespace('Auth')->group(function() {
    Route::post('/login', 'LoginController@login')->name('login');
  });
});

コントローラー

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

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use App\Models\User;  // 追加

class LoginController extends Controller
{

  //中略

  public function username()
  {
    return 'login_id';
  }

  /**
   * @param Request $request
   * @param User $user
   * @return mixed
   */
  protected function authenticated(Request $request, User $user)
  {
    return $user;
  }
}

ミドルウェアの修正

app/Http/Middleware/RedirectIfAuthenticated.php
<?php

namespace App\Http\Middleware;

use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Support\Facades\Auth;

class RedirectIfAuthenticated
{
  /**
   * Handle an incoming request.
   *
   * @param \Illuminate\Http\Request $request
   * @param \Closure $next
   * @param string|null $guard
   * @return mixed
   */
  public function handle($request, Closure $next, $guard = null)
  {
    if (Auth::guard($guard)->check()) {
      return redirect()->route('current_user');  // 変更
    }

    return $next($request);
  }
}

corsの設定

/var/www/laravel
/var/www/laravel # composer require fruitcake/laravel-cors
app/Http/Kernel.php
protected $middleware = [
  // \App\Http\Middleware\TrustHosts::class,
  \App\Http\Middleware\TrustProxies::class,
  \Fruitcake\Cors\HandleCors::class,
  \App\Http\Middleware\CheckForMaintenanceMode::class,
  \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
  \App\Http\Middleware\TrimStrings::class,
  \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
  \Fruitcake\Cors\HandleCors::class,  // 追加
];
.env
CORS_ALLOWED_ORIGIN=http://localhost:3000
/var/www/laravel
/var/www/laravel # php artisan vendor:publish --tag="cors"
config/cors.php
<?php

return [

  /*
    |--------------------------------------------------------------------------
    | Cross-Origin Resource Sharing (CORS) Configuration
    |--------------------------------------------------------------------------
    |
    | Here you may configure your settings for cross-origin resource sharing
    | or "CORS". This determines what cross-origin operations may execute
    | in web browsers. You are free to adjust these settings as needed.
    |
    | To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
    |
    */


  'paths' => ['*'],

  'allowed_methods' => ['*'],

  'allowed_origins' => ['*'],

  'allowed_origins_patterns' => [],

  'allowed_headers' => ['*'],

  'exposed_headers' => [],

  'max_age' => 0,

  'supports_credentials' => true,

];
/var/www/laravel
/var/www/laravel # php artisan config:cache 

Nuxt側の実装

nuxt-property-decoratorのインストール

~/workspace/myapp
$ docker-compose exec front_app ash
/var/www/nuxt # yarn add -D nuxt-property-decorator

ログインに最低限必要なファイル作成

~/workspace/myapp/nuxt
$ mkdir pages/mypage
$ touch pages/{login,mypage/index}.vue \
> store/{index,auth}.ts \
> middleware/{guestCheck,loginCheck}.ts

ストアにログイン処理

store/auth.ts
import { GetterTree, ActionTree, MutationTree } from 'vuex'

export const state = () => ({
  user: null,
  statusCode: null,
  messages: null,
})

export type RootState = ReturnType<typeof state>

export const mutations: MutationTree<RootState> = {
  setUser(state, user) {
    state.user = user
  },
  setStatus(state, error) {
    state.statusCode = error.status
    state.messages = error.messages
  },
  clearStatus(state) {
    state.statusCode = null
    state.messages = null
  },
}

export const getters: GetterTree<RootState, RootState> = {
  currentUser(state) {
    return state.user
  },
  isLogin(state): boolean {
    return !!state.user
  },
  isError(state): boolean {
    return state.messages === null
  },
}

export const actions: ActionTree<RootState, RootState> = {
  async login({ commit, dispatch }, submitData): Promise<void> {
    await this.$axios
      .$post('/login', submitData)
      .then((response): void => {
        commit('status/clearStatus')
        commit('setUser', response)
      })
      .catch((err): void => {
        dispatch('errorHandler', err)
      })
  },
  errorHandler({ commit, dispatch }, err): void {
    commit('setStatus', {
      status: err.response.status,
      messages: err.response.data.errors,
    })
  },
}

状態維持

store/index.ts
export const actions = {
  async nuxtServerInit({ commit }, { app }): Promise<void> {
    await app.$axios
      .$get('/current_user')
      .then((user): void => commit('auth/setUser', user))
      .catch(() => commit('auth/setUser', null))
  },
}

ミドルウェア

認証済の場合はへマイページ画面にリダイレクトさせる

middleware/loginCheck.ts
import { Middleware } from '@nuxt/types'

const loginCheck: Middleware = ({ store, redirect }): void => {
  if (store.state.auth.user) {
    redirect('/mypage')
  }
}

export default loginCheck

認証済でない場合はへログイン画面にリダイレクトさせる

middleware/guestCheck.ts
import { Middleware } from '@nuxt/types'
const guestCheck: Middleware = ({ store, redirect, route }): void => { 
  if (!store.state.auth.user) {
    redirect({
      name: 'login',
    })
  }
}

export default guestCheck

ログイン画面へのリンク〜ログイン後のマイページへの画面遷移

pages/index.vue
<template>
  <div>
    <router-link to="/login" tag="a">ログイン</router-link>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator';

@Component
export default class Index extends Vue {}
</script>
pages/login.vue
<template>
  <div class="login">
    <h1>ログイン画面</h1>
    <form @submit.prevent="login">
      <div class="row">
        <input type="text" v-model="loginData.login_id">
      </div>
      <div class="row">
        <input type="password" v-model="loginData.password">
      </div>
      <div class="row">
        <button>ログイン</button>
      </div>
    </form>
  </div>
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
@Component({
  middleware: 'loginCheck',
})
export default class Login extends Vue {
  loginData: object = {
    login_id: '',
    password: '',
  }

  async login(): Promise<void> {
    const submitData = new FormData()
    submitData.append('login_id', this.loginData.login_id)
    submitData.append('password', this.loginData.password)
    await this.$store.dispatch('auth/login', submitData)
    if (this.$store.getters['auth/isLogin']) {
      this.$router.push('/mypage')
    }
  }
}
</script>
pages/mypage/index.vue
<template>
  <div>
    <h1>{{ user.name }}さん、こんにちは</h1>
  </div>
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import { mapGetters } from 'vuex'

@Component({
  middleware: 'guestCheck',
  computed: {
    ...mapGetters({
      user: 'auth/currentUser',
    })
  }
})
export default class Index extends Vue {}
</script>

画面確認

認証後にログイン画面に行こうとするとマイページにリダイレクトされるところまで

login.gif

ログアウト

Laravel側の実装

routes/api.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Auth;

Route::group(["middleware" => "api"], function () {
  Route::post('/login', 'Auth\LoginController@login')->name('login');
  Route::get('/current_user', function () {
    return Auth::user();
  })->name('current_user');
  // 追加
  Route::middleware('auth:api')->group(function() {
    Route::post('/logout', 'Auth\LoginController@logout')->name('logout');
  });
});
app/Http/Controllers/Auth/LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{

  //中略

  public function username()
  {
    return 'login_id';
  }

  /**
   * @param Request $request
   * @param $user
   * @return mixed
   */
  protected function authenticated(Request $request, $user)
  {
    return $user;
  }

  // 追加
  /**
   * @param Request $request
   * @return \Illuminate\Http\JsonResponse
   */
  protected function loggedOut(Request $request)
  {
    Auth::logout();
    return response()->json();
  }

}

Nuxt側の実装

store/auth.ts
import { GetterTree, ActionTree, MutationTree } from 'vuex'

// 中略

export const actions: ActionTree<RootState, RootState> = {
  async login(
    { commit, dispatch },
    submitData
  ): Promise<void> {
    await this.$axios
      .$post('/login', submitData)
      .then((response): void => {
        commit('setUser', response)
      })
      .catch((err): void => {
        dispatch('errorHandler', err)
      })
  },
  // 追加
  async logout({ commit, dispatch }): Promise<void> {
    await this.$axios
      .$post('/logout')
      .then(() => {
        commit('setUser', null)
      })
      .catch((err): void => {
        dispatch('status/errorHandler', err)
      })
  },
  errorHandler({ commit, dispatch }, err): void {
    commit('setStatus', {
      status: err.response.status,
      messages: err.response.data.errors,
    })
  },
}

pages/mypage/index.vue
<template>
  <div>
    <h1>{{ user.name }}さん、こんにちは</h1>
    <!-- ↓追加 -->
    <button @click="logout">ログアウト</button>
  </div>
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import { mapGetters } from 'vuex'

@Component({
  middleware: 'guestCheck',
  computed: {
    ...mapGetters({
      user: 'auth/currentUser',
    })
  }
})
export default class Index extends Vue {
  // ↓追加
  async logout(): Promise<void> {
    await this.$store.dispatch('auth/logout')
    if(!this.user) {
      this.$router.push('/login')
    }
  }
}
</script>

画面確認

認証されていない状態でマイページに行こうとするとログイン画面に戻されるところまで

logout.gif

10
5
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
10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?