Система аутентификации - важная часть одностраничного приложения. Однако, прежде чем мы начнем, давай отредактируем метод API\AuthController@login из первой части урока. Я осознал, что сейчас он сделан немного неграмотно. Наверняка нам понадобится информация о только что авторизированном пользователе, а не только токены. Ниже отредактированный код. Я добавил комментарии, так что дополнительные пояснения излишни.

public function login()
{
    // Проверяем существует ли пользователь с указанным email адресом
    $user = User::whereEmail(request('username'))->first();

    if (!$user) {
        return response()->json([
            'message' => 'Wrong email or password',
            'status' => 422
        ], 422);
    }

    // Если пользователь с таким email адресом найден - проверим совпадает
    // ли его пароль с указанным
    if (!Hash::check(request('password'), $user->password)) {
        return response()->json([
            'message' => 'Wrong email or password',
            'status' => 422
        ], 422);
    }

    // Внутренний API запрос для получения токенов
    $client = DB::table('oauth_clients')
        ->where('password_client', true)
        ->first();

    // Убедимся, что Password Client существует в БД (т.е. Laravel Passport
    // установлен правильно)
    if (!$client) {
        return response()->json([
            'message' => 'Laravel Passport is not setup properly.',
            'status' => 500
        ], 500);
    }

    $data = [
        'grant_type' => 'password',
        'client_id' => $client->id,
        'client_secret' => $client->secret,
        'username' => request('username'),
        'password' => request('password'),
    ];

    $request = Request::create('/oauth/token', 'POST', $data);

    $response = app()->handle($request);

    // Проверяем был ли внутренний запрос успешным
    if ($response->getStatusCode() != 200) {
        return response()->json([
            'message' => 'Wrong email or password',
            'status' => 422
        ], 422);
    }

    // Вытаскиваем данные из ответа
    $data = json_decode($response->getContent());

    // Формируем окончательный ответ в нужном формате
    return response()->json([
        'token' => $data->access_token,
        'user' => $user,
        'status' => 200
    ]);
}

Теперь давайте займемся страницей входа. Отредактируйте Login.vue следующим образом:

<template>
    <div>
        <h1>Login</h1>

        <p>
            <label for="username">Email</label>

            <input type="text" name="username" v-model="username">
        </p>

        <p>
            <label for="password">Password</label>

            <input type="password" name="password" v-model="password">
        </p>

        <button @click="login">Login</button>
    </div>
</template>

<script>
export default {
    data() {
        return {
            username: '',
            password: '',
        };
    },

    methods: {
        login() {
            let data = {
                username: this.username,
                password: this.password
            };

            axios.post('/api/login', data)
                .then(({data}) => {
                    // TODO: сохранить полученные данные
                    // data.token
                    // data.user
                })
                .catch(({response}) => {                    
                    alert(response.data.message);
                });
        }
    }
}
</script>

Это простая форма, которая отсылает AJAX запрос с помощью axios (который установлен в Laravel по-умолчанию) на наш маршрут /api/login. Базовых знаний Vue.js будет достаточно для того, чтобы понять этот код.

Итак, мы получаем токен и информацию о пользователе в ответе. Нам нужно сохранить эти данные так, чтобы они не потерялись после перезагрузки страницы или если пользователь закроет браузер. Мы будем хранить данные в локальном хранилище HTML5. Этот метод хранения уязвимыми к XSS атакам, а чтобы предотвратить XSS атаки, нужно все время перепроверять данные в backend'е. Есть и другой метод, но и он не на 100% безопасен. Подробнее о проблеме можно почитать в этой статье (на английском).

Давайте создадим класс Auth на ES6, который будет отвечать за вопросы, связанные с аутентификацией. Что-то вроде класса Auth в Laravel. Создайте файл resources/assets/js/auth.js, который будет синглтоном.

class Auth {
    constructor() {

    }
}

export default new Auth();

Сделайте объект класса доступным глобально, добавив следующий код вверху файла resources/assets/js/app.js:

import auth from './auth.js';

window.auth = auth;

Дальше, давайте откроем auth.js и создадим метод login, который просто будет сохранять полученные данные.

class Auth {
    constructor() {

    }

    login (token, user) {        
        window.localStorage.setItem('token', token);
        window.localStorage.setItem('user', JSON.stringify(user));

        axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
    }
}

Работать с локальным хранилищем HTML5 очень просто. Это простое хранилище пар ключ-значение, где значения могут быть только строковыми. Так как user это объект, нам нужно привести его в строковый формат перед сохранением. После сохранения данных мы задаем заголовок Authorization в axios, чтобы токен был включен в запрос при последующих AJAX запросах.

Наконец, нам нужно вызвать новый метод в файле Login.vue сразу после успешного запроса к API. Также было бы логичным перенаправить пользователя в зону приложения для авторизированных пользователей, в нашем случае это страница по адресу /dashboard. Редирект выполняется с помощью метода push маршрутизатора, доступ же к маршрутизатору можно получить из любого Vue файла через this.$router.

axios.post('/api/login', data)
    .then(({data}) => {
        auth.login(data.token, data.user);

        this.$router.push('/dashboard');
    })  
    .catch(({response}) => {                    
        alert(response.data.message);
    });

Отлично, теперь нам нужно обновить пользовательский интерфейс, чтобы отобразить, что пользователь вошел в систему. Как минимум нам нужно заменить ссылку Login на Logout и, возможно, показать имя пользователя где-нибудь. Наш объект auth служит нам глобальным хранилищем, но, к сожалению, Vue не может отслеживать изменения в нем. Чтобы Vue был в курсе изменений, нам придется создать глобальный менеджер событий, который будет просто еще одним объектом Vue. Сделаем это в файле app.js.

Vue.use(VueRouter);

window.Event = new Vue;

Теперь нам нужно добавить кое-что в класс auth.js. Для начала добавим 2 свойства для хранения данных. Отредактируйте конструктор следующим образом:

constructor () {
    this.token = null;
    this.user = null;
}

Мы будем задавать эти значения в конце метода login. После, вызовем глобальное событие.

...
login (token, user) {        
    window.localStorage.setItem('token', token);
    window.localStorage.setItem('user', JSON.stringify(user));

    axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;

    this.token = token;
    this.user = user;

    Event.$emit('userLoggedIn');
}

Наконец, давайте добавим вспомогательный метод, который будет проверять авторизирован ли пользователь в данный момент времени. В методе мы просто будем проверять, задано ли значение у свойства this.token.

check () {
    return !! this.token;
}

Кстати, вы наверняка подумали, что наша система не безопасна - любой может выполнить auth.login('token', {}); в консоли браузера и интерфейс нашего приложения изменится на интерфейс авторизированного пользователя, дав доступ к страницам, которые без авторизации не должны быть доступны. Отчасти это так, но помните, что наш сервер вернет 401 после первого же AJAX запроса с подставным токеном, так что никаких реальных изменений на стороне backend'а не произойдет. Что же касается изменения в таком случае интерфейса обратно на интерфейс неавторизированного пользователя - мы займемся этим позже.

Итак, единственный файл, который остается отредактировать - это Layout.vue. Сначала отредактируйте блок <script>:

<script>
export default {
    data() {
        return {
            authenticated: auth.check(),
            user: auth.user
        };
    },

    mounted() {
        Event.$on('userLoggedIn', () => {
            this.authenticated = true;
            this.user = auth.user;
        });
    },
}
</script>

Мы считываем изначалный статус аутентификации при загрузке страницы. Затем, в методе mounted() мы добавляем слушатель для глобального события userLoggedIn и заного считываем данные из глобально объекта auth, когда событие срабатывает. С этим должно быть понятно, дальше замените ссылку Login на вот это:

<div v-if="authenticated && user">
    <p>Hello, {{ user.name }}</p>

    <router-link to="/logout">Logout</router-link>
</div>

<router-link to="/login" v-else>Login</router-link>

Если свойство authenticated равно true и объект user не null и не undefined - показываем блок с именем пользователя и ссылкой Logout. Иначе, показываем ссылку Login без имени пользователя.

У нас осталось еще несколько моментов, о которых нужно позаботиться. Но для начала, почему бы вам не сделать функционал выхода из системы самостоятельно в качестве домашней работы? По большому счету достаточно просто сделать шаги из метода login() в обратном порядке. Просто очистите все данные, задав переменным пустые строки или null, и перенаправьте пользователя на главную страницу (home). Увидимся в следующей части!