29 июля 2022 г. Первоначально опубликовано на techvblogs.com・Чтение: 11 мин.

Laravel Sanctum предоставляет облегченную систему аутентификации, основанную на встроенных в Laravel службах аутентификации сеансов на основе файлов cookie.

Как работает Laravel Sanctum

Прежде чем мы начнем слепо месить, не понимая, что происходит за кулисами, давайте пробежимся по тому, как работает Sanctum.

Laravel Sanctum использует сеансовую аутентификацию Laravel на основе файлов cookie для аутентификации пользователей вашего клиента. Вот поток.

  1. Вы запрашиваете файл cookie CSRF из Sanctum на клиенте, что позволяет вам делать запросы, защищенные CSRF, к обычным конечным точкам, таким как /login.
  2. Вы делаете запрос к обычной конечной точке Laravel/login.
  3. Laravel создает файл cookie, содержащий сеанс пользователя.
  4. Любые запросы к вашему API теперь включают этот файл cookie, поэтому ваш пользователь аутентифицируется на время существования этого сеанса.

Аутентификация SPA с использованием Laravel 9 Sanctum, Vue 3 и Vite Пример:

  1. Создать проект Laravel
  2. Настройка сведений о базе данных
  3. Установить laravel/ui
  4. Установите Вью 3
  5. Установите плагин vitejs/plugin-vue
  6. Обновить файл vite.config.js
  7. Импортировать путь начальной загрузки в vite.config.js
  8. Установите зависимости NPM
  9. Обновить bootstrap.js
  10. Импорт Bootstrap 5 SCSS в папку JS
  11. Запуск сервера Vite Dev
  12. Установить Laravel Sanctum
  13. Настройка Laravel Sanctum
  14. Перенести базу данных
  15. Настройка внешнего интерфейса

Требования

  1. PHP ^8.0
  2. Ларавель ^9.0
  3. MySQL
  4. Начальная загрузка 5
  5. Вью 3
  6. Вите

В этом блоге мы вместе создадим полную функцию регистрации и входа для одностраничного приложения с Laravel 9 Sanctum, Bootstrap5, Vue 3 и Vite.

Шаг 1: Создайте проект Laravel

Сначала откройте терминал и выполните следующую команду, чтобы создать новый проект Laravel:

composer create-project --prefer-dist laravel/laravel:^9.0 lara9sanctum-vue3-vite

или, если вы установили установщик Laravel как глобальную зависимость композитора:

laravel new lara9sanctum-vue3-vite

Шаг 2. Настройка сведений о базе данных

открыть .env и обновить детали базы данных

DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=<DATABASE NAME>
DB_USERNAME=<DATABASE USERNAME>
DB_PASSWORD=<DATABASE PASSWORD>

Шаг 3. Установите laravel/ui

composer require laravel/ui
php artisan ui vue --auth

Шаг 4: Установите Vue 3

Теперь после установки узловых модулей нам нужно установить vue 3 в наше приложение, для этого выполните следующую команду в терминале npm install vue@next vue-loader@next. vue-loader — это загрузчик для веб-пакета, который позволяет вам создавать компоненты Vue в формате, называемом Single-File Components. vue-loader@next — это загрузчик, предназначенный для webpack для создания компонентов Vue в однофайловых компонентах, называемых SFC.

npm install vue@next vue-loader@next

Шаг 5: Установите плагин vitejs/plugin-vue

В последнем выпуске laravel 9 установите плагин vitejs/plugin-vue для установки vue3 или vue в laravel. Этот плагин предоставляет необходимые зависимости для запуска приложения vuejs на vite. Vite — это команда сборки, которая связывает ваш код с Rollup и запускает порт localhost: 3000, чтобы обеспечить функцию горячего обновления.

npm i @vitejs/plugin-vue

Шаг 6: Обновите файл vite.config.js

Vite — это сборщик модулей для современных приложений JavaScript. Откройте vite.config.js и скопируйте и вставьте следующий код. Сначала выставьте счет defineConfig из vite вверху файла, а также импортируйте laravel-vite-plugin. Здесь plugins() выберите путь к файлу js и CSS и создайте пакеты для своего приложения. вам нужно добавить vue() в массив плагинов.

// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue'

export default defineConfig({
    plugins: [
        vue(),
        laravel([
            'resources/js/app.js',
        ]),
    ],
});

Шаг 7: Импортируйте путь начальной загрузки в vite.config.js

Во-первых, вам нужно изменить vite.config.js и добавить путь начальной загрузки 5 и удалить resources/css/app.css

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
    plugins: [
        vue(),
        laravel([
            'resource/scss/app.scss',
            'resources/js/app.js',
        ]),
    ],
    resolve: {
        alias: {
            '~bootstrap': path.resolve(__dirname, 'node_modules/bootstrap'),
            '@': '/resources/js',
        }
    },
});

Шаг 8: Установите зависимости NPM

Выполните следующую команду, чтобы установить внешние зависимости:

npm install

Шаг 9: Обновите bootstrap.js

Нам нужно использовать import вместо require.

import loadash from 'lodash'
window._ = loadash
import * as Popper from '@popperjs/core'
window.Popper = Popper
import 'bootstrap'
/**
 * We'll load the axios HTTP library which allows us to easily issue requests
 * to our Laravel back-end. This library automatically handles sending the
 * CSRF token as a header based on the value of the "XSRF" token cookie.
 */
import axios from 'axios'
window.axios = axios
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
 * Echo exposes an expressive API for subscribing to channels and listening
 * for events that are broadcast by Laravel. Echo and event broadcasting
 * allows your team to easily build robust real-time web applications.
 */
/*import Echo from 'laravel-echo';
window.Pusher = require('pusher-js');
window.Echo = new Echo({
     broadcaster: 'pusher',
     key: process.env.MIX_PUSHER_APP_KEY,
     cluster: process.env.MIX_PUSHER_APP_CLUSTER,
     forceTLS: true
});*/

Шаг 10: Импортируйте Bootstrap 5 SCSS в папку JS

Теперь вам нужно импортировать путь bootstrap 5 SCSS в resources/js/app.js.

ресурсы/js/app.js

import './bootstrap';
import '../sass/app.scss'

Шаг 11: Запуск сервера Vite Dev

Теперь, после установки vue 3, нам нужно запустить сервер разработки для vite, для этого выполните следующую команду, и он будет просматривать ваш файл resources/js/app.js и файл resources/css/app.css. Он также запускает vite-сервер на http://localhost:3000. вы не можете открыть его в браузере, так как он предназначен для горячей перезагрузки, работает в фоновом режиме и просматривает активы вашего приложения, такие как js и CSS.

npm run dev

Шаг 12: Установите Laravel Sanctum

Вы можете найти документацию на официальном сайте Laravel.

composer require laravel/sanctum

Шаг 13: Настройте Laravel Sanctum

Откройте config/sanctum.php и обновите следующий код:

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),

Вам нужно будет изменить это при развертывании в рабочей среде, поэтому добавление SANCTUM_STATEFUL_DOMAINS в файл .env со списком разрешенных доменов, разделенных запятыми, — отличная идея.

Откройте .env file и добавьте эту строку

SANCTUM_STATEFUL_DOMAINS=localhost:<PORT NUMBER>

Изменить драйвер сеанса

В .env обновите драйвер сеанса file до cookie.

SESSION_DRIVER=cookie

Настроить CORS

Откройте config/cors.php и обновите в файле следующий код:

'paths' => [
    'api/*',
    '/login',
    '/logout',
    '/sanctum/csrf-cookie'
],

Также установите для параметра supports_credentials option значение true:

'supports_credentials' => true,

Давайте создадим наш компонент Vue, который будет содержать нашу форму входа и отображать некоторые секреты.

Шаг 14: Перенесите базу данных

php artisan migrate

Шаг 15: Настройка внешнего интерфейса

Когда мы сгенерировали наш внешний код ранее, используя php artisan ui vue, пример компонента был сгенерирован под resources/js/components/ExampleComponent.vue. Давайте создадим другие компоненты для входа в систему, регистрации и страницы панели инструментов.

Что такое Vue Router?

Vue Router помогает связать URL/Историю браузера и компоненты Vue, позволяя определенным путям отображать любое представление, связанное с ним.

Особенности Vue Router

  • Вложенные маршруты
  • Параметры маршрута, запрос
  • Динамическое сопоставление маршрутов
  • Ссылки с автоматическими активными классами CSS
  • и многое другое

Давайте установим vue-router

npm install vue-router

Теперь создайте компоненты для входа и регистрации.

Создайте файл внутри папки resources/js/components с именем Login.vue .

resources/js/components/Login.vue

<template>
    <div class="container h-100">
        <div class="row h-100 align-items-center">
            <div class="col-12 col-md-6 offset-md-3">
                <div class="card shadow sm">
                    <div class="card-body">
                        <h1 class="text-center">Login</h1>
                        <hr/>
                        <form action="javascript:void(0)" class="row" method="post">
                            <div class="col-12" v-if="Object.keys(validationErrors).length > 0">
                                <div class="alert alert-danger">
                                    <ul class="mb-0">
                                        <li v-for="(value, key) in validationErrors" :key="key">{{ value[0] }}</li>
                                    </ul>
                                </div>
                            </div>
                            <div class="form-group col-12">
                                <label for="email" class="font-weight-bold">Email</label>
                                <input type="text" v-model="auth.email" name="email" id="email" class="form-control">
                            </div>
                            <div class="form-group col-12 my-2">
                                <label for="password" class="font-weight-bold">Password</label>
                                <input type="password" v-model="auth.password" name="password" id="password" class="form-control">
                            </div>
                            <div class="col-12 mb-2">
                                <button type="submit" :disabled="processing" @click="login" class="btn btn-primary btn-block">
                                    {{ processing ? "Please wait" : "Login" }}
                                </button>
                            </div>
                            <div class="col-12 text-center">
                                <label>Don't have an account? <router-link :to="{name:'register'}">Register Now!</router-link></label>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
    name:"login",
    data(){
        return {
            auth:{
                email:"",
                password:""
            },
            validationErrors:{},
            processing:false
        }
    },
    methods:{
        ...mapActions({
            signIn:'auth/login'
        }),
        async login(){
            this.processing = true
            await axios.get('/sanctum/csrf-cookie')
            await axios.post('/login',this.auth).then(({data})=>{
                this.signIn()
            }).catch(({response})=>{
                if(response.status===422){
                    this.validationErrors = response.data.errors
                }else{
                    this.validationErrors = {}
                    alert(response.data.message)
                }
            }).finally(()=>{
                this.processing = false
            })
        },
    }
}
</script>

Создайте файл внутри папки resources/js/components с именем Register.vue.

<template>
    <div class="container h-100">
        <div class="row h-100 align-items-center">
            <div class="col-12 col-md-6 offset-md-3">
                <div class="card shadow sm">
                    <div class="card-body">
                        <h1 class="text-center">Register</h1>
                        <hr/>
                        <form action="javascript:void(0)" @submit="register" class="row" method="post">
                            <div class="col-12" v-if="Object.keys(validationErrors).length > 0">
                                <div class="alert alert-danger">
                                    <ul class="mb-0">
                                        <li v-for="(value, key) in validationErrors" :key="key">{{ value[0] }}</li>
                                    </ul>
                                </div>
                            </div>
                            <div class="form-group col-12">
                                <label for="name" class="font-weight-bold">Name</label>
                                <input type="text" name="name" v-model="user.name" id="name" placeholder="Enter name" class="form-control">
                            </div>
                            <div class="form-group col-12 my-2">
                                <label for="email" class="font-weight-bold">Email</label>
                                <input type="text" name="email" v-model="user.email" id="email" placeholder="Enter Email" class="form-control">
                            </div>
                            <div class="form-group col-12">
                                <label for="password" class="font-weight-bold">Password</label>
                                <input type="password" name="password" v-model="user.password" id="password" placeholder="Enter Password" class="form-control">
                            </div>
                            <div class="form-group col-12 my-2">
                                <label for="password_confirmation" class="font-weight-bold">Confirm Password</label>
                                <input type="password_confirmation" name="password_confirmation" v-model="user.password_confirmation" id="password_confirmation" placeholder="Enter Password" class="form-control">
                            </div>
                            <div class="col-12 mb-2">
                                <button type="submit" :disabled="processing" class="btn btn-primary btn-block">
                                    {{ processing ? "Please wait" : "Register" }}
                                </button>
                            </div>
                            <div class="col-12 text-center">
                                <label>Already have an account? <router-link :to="{name:'login'}">Login Now!</router-link></label>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
    name:'register',
    data(){
        return {
            user:{
                name:"",
                email:"",
                password:"",
                password_confirmation:""
            },
            validationErrors:{},
            processing:false
        }
    },
    methods:{
        ...mapActions({
            signIn:'auth/login'
        }),
        async register(){
            this.processing = true
            await axios.get('/sanctum/csrf-cookie')
            await axios.post('/register',this.user).then(response=>{
                this.validationErrors = {}
                this.signIn()
            }).catch(({response})=>{
                if(response.status===422){
                    this.validationErrors = response.data.errors
                }else{
                    this.validationErrors = {}
                    alert(response.data.message)
                }
            }).finally(()=>{
                this.processing = false
            })
        }
    }
}
</script>

Создайте компонент макета для всех аутентифицированных страниц. Таким образом, нам не нужно добавлять верхний и нижний колонтитулы и любой другой компонент во все страницы, поэтому здесь мы создали компонент макета с именем Dashboard.vue. Здесь, в компоненте, мы добавляем заголовок, нижний колонтитул и представление маршрутизатора, чтобы каждый компонент отображался в этом представлении маршрутизатора.

resources/js/components/layouts/Default.vue

<template>
    <div>
        <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
            <div class="container-fluid">
                <a class="navbar-brand" href="https://techvblogs.com/blog/spa-authentication-laravel-9-sanctum-vue3-vite" target="_blank">TechvBlogs</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbarNavDropdown">
                    <ul class="navbar-nav me-auto">
                        <li class="nav-item">
                            <router-link :to="{name:'dashboard'}" class="nav-link">Home <span class="sr-only">(current)</span></router-link>
                        </li>
                    </ul>
                    <div class="d-flex">
                        <ul class="navbar-nav">
                            <li class="nav-item dropdown">
                                <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                    {{ user.name }}
                                </a>
                                <div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdownMenuLink">
                                    <a class="dropdown-item" href="javascript:void(0)" @click="logout">Logout</a>
                                </div>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </nav>
        <main class="mt-3">
            <router-view></router-view>
        </main>
    </div>
</template>
<script>
import {mapActions} from 'vuex'
export default {
    name:"default-layout",
    data(){
        return {
            user:this.$store.state.auth.user
        }
    },
    methods:{
        ...mapActions({
            signOut:"auth/logout"
        }),
        async logout(){
            await axios.post('/logout').then(({data})=>{
                this.signOut()
                this.$router.push({name:"login"})
            })
        }
    }
}
</script>

resources/js/components/Dashboard.vue

<template>
    <div class="container">
        <div class="row">
            <div class="col-12">
                <div class="card shadow-sm">
                    <div class="card-header">
                        <h3>Dashboard</h3>
                    </div>
                    <div class="card-body">
                        <p class="mb-0">You are logged in as <b>{{user.email}}</b></p>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
export default {
    name:"dashboard",
    data(){
        return {
            user:this.$store.state.auth.user
        }
    }
}
</script>

Теперь добавьте этот компонент страницы к маршрутизатору.

Создайте новый файл resources/js/router/index.js

import { createWebHistory, createRouter } from 'vue-router'
import store from '@/store'
/* Guest Component */
const Login = () => import('@/components/Login.vue')
const Register = () => import('@/components/Register.vue')
/* Guest Component */
/* Layouts */
const DahboardLayout = () => import('@/components/layouts/Default.vue')
/* Layouts */
/* Authenticated Component */
const Dashboard = () => import('@/components/Dashboard.vue')
/* Authenticated Component */

const routes = [
    {
        name: "login",
        path: "/login",
        component: Login,
        meta: {
            middleware: "guest",
            title: `Login`
        }
    },
    {
        name: "register",
        path: "/register",
        component: Register,
        meta: {
            middleware: "guest",
            title: `Register`
        }
    },
    {
        path: "/",
        component: DahboardLayout,
        meta: {
            middleware: "auth"
        },
        children: [
            {
                name: "dashboard",
                path: '/',
                component: Dashboard,
                meta: {
                    title: `Dashboard`
                }
            }
        ]
    }
]
const router = createRouter({
    history: createWebHistory(),
    routes, // short for `routes: routes`
})
router.beforeEach((to, from, next) => {
    document.title = to.meta.title
    if (to.meta.middleware == "guest") {
        if (store.state.auth.authenticated) {
            next({ name: "dashboard" })
        }
        next()
    } else {
        if (store.state.auth.authenticated) {
            next()
        } else {
            next({ name: "login" })
        }
    }
})
export default router

Добавить роутер в resources/js/app.js

import './bootstrap';
import '../sass/app.scss'
import Router from '@/router'
import { createApp } from 'vue/dist/vue.esm-bundler';
const app = createApp({})
app.use(Router)
app.mount('#app')

Прежде чем мы отправим эти запросы, нам нужно установить базовый URL-адрес для нашего API (обратите внимание, что они не включены в запросы, которые у нас есть прямо сейчас), а также включить параметр withCredentials.

Откройте resources/js/bootstrap.js и добавьте в этот файл следующий код:

window.axios.defaults.withCredentials = true

Параметр withCredentials здесь действительно важен. Этот Axios предписывает автоматически отправлять наш файл cookie для аутентификации вместе с каждым запросом.

Что такое Векс?

Vuex — это шаблон управления состоянием + библиотека для Vue. js-приложения. Он служит централизованным хранилищем для всех компонентов приложения с правилами, гарантирующими, что состояние может изменяться только предсказуемым образом.

Что ж, поскольку мы хотим сохранить общее аутентифицированное «состояние» в нашем клиенте, использование библиотеки управления состоянием, такой как Vuex, имеет смысл здесь. Это также позволит нам легко проверить в любом компоненте, аутентифицированы мы или нет (например, наша навигация).

Давайте установим Vuex

npm install vuex --save

Сначала создайте файл resources/js/store/auth.js со следующим.

import axios from 'axios'
import router from '@/router'
export default {
    namespaced: true,
    state:{
        authenticated:false,
        user:{}
    },
    getters:{
        authenticated(state){
            return state.authenticated
        },
        user(state){
            return state.user
        }
    },
    mutations:{
        SET_AUTHENTICATED (state, value) {
            state.authenticated = value
        },
        SET_USER (state, value) {
            state.user = value
        }
    },
    actions:{
        login({commit}){
            return axios.get('/api/user').then(({data})=>{
                commit('SET_USER',data)
                commit('SET_AUTHENTICATED',true)
                router.push({name:'dashboard'})
            }).catch(({response:{data}})=>{
                commit('SET_USER',{})
                commit('SET_AUTHENTICATED',false)
            })
        },
        logout({commit}){
            commit('SET_USER',{})
            commit('SET_AUTHENTICATED',false)
        }
    }
}

Свойство state указывает, прошли ли мы аутентификацию или нет, и содержит сведения о пользователе, которые мы будем получать после аутентификации.

Наши getters возвращают нам это состояние.

Наш mutations обновил наш state. Например, как только мы успешно прошли аутентификацию, мы зафиксируем мутацию, чтобы установить для аутентифицированного значение true, и совершим еще одну мутацию, чтобы установить данные пользователя.

Иногда нам нужно, чтобы наше веб-приложение VueJS сохраняло некоторую информацию в локальном хранилище браузера. Это могут быть локальные настройки, информация об учетной записи или какие-то токены. Мы определенно не хотим потерять их после обновления страницы. Вот почему нам нужно использовать vuex-persistedstate.

Установить vuex-persistedstate

npm i vuex-persistedstate

Теперь добавьте модуль авторизации в Vuex в resources/js/store/index.js.

import { createStore } from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import auth from '@/store/auth'
const store = createStore({
    plugins:[
        createPersistedState()
    ],
    modules:{
        auth
    }
})
export default store

Добавьте Vuex в resources/js/app.js

import './bootstrap';
import '../sass/app.scss'
import Router from '@/router'
import store from '@/store'
import { createApp } from 'vue/dist/vue.esm-bundler';
const app = createApp({})
app.use(Router)
app.use(store)
app.mount('#app')

откройте resources/views/welcome.blade.php и замените этот код:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>SPA Authentication using Laravel 9 Sanctum, Vue 3 and Vite - TechvBlogs</title>
        <!-- Fonts -->
        <link href="https://fonts.bunny.net/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
        @vite(['resources/js/app.js'])
    </head>
    <body>
        <div id="app">
            <router-view></router-view>
        </div>
    </body>
</html>

Теперь определите маршруты в файлах маршрутов web.php и api.php. Перейдите в папку routes, откройте файл web.php и обновите следующие маршруты:

маршруты / web.php

<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('{any}', function () {
    return view('welcome');
})->where('any', '.*');
Auth::routes();
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');

Теперь пришло время запустить наш проект.

php artisan serve

Откройте localhost:‹НОМЕР ПОРТА› в браузере.

Было бы неплохо использовать простое демонстрационное приложение, которое можно найти в этом репозитории GitHub.

Спасибо за чтение этого блога.