はじめに
最近個人開発した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
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;
}