前提
ローカル環境
- 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>
画面確認
認証後にログイン画面に行こうとするとマイページにリダイレクトされるところまで
ログアウト
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>
画面確認
認証されていない状態でマイページに行こうとするとログイン画面に戻されるところまで