Статьи

Как добавить аутентификацию в ваше приложение Vue с помощью Okta

Эта статья была первоначально опубликована в блоге разработчиков Okta . Спасибо за поддержку партнеров, которые делают возможным использование SitePoint.

Я танцевал фреймворк JavaScript в течение многих лет, начиная с jQuery, а затем до Angular. Разочаровавшись в сложности Angular, я нашел React и подумал, что я в чистоте. То, что на первый взгляд казалось простым, в итоге привело к неприятному беспорядку Тогда я нашел Vue.js. Это было просто правильно. Сработало как положено. Это было быстро. Документация была невероятной. Шаблонирование было красноречивым. Было достигнуто единодушное согласие относительно того, как обрабатывать управление состояниями, условное отображение, двустороннее связывание, маршрутизацию и многое другое.

В этом учебном пособии вы шаг за шагом проведете работу по созданию леса проекта Vue.js, разгрузке безопасной аутентификации в OpenID Connect API (OIDC) Okta , блокировке защищенных маршрутов и выполнению операций CRUD через сервер REST API бэкэнда. Этот учебник использует следующие технологии, но не требует глубоких знаний, чтобы следовать:

О Vue.js

Vue.js — это надежная, но простая среда Javascript. Он имеет один из самых низких барьеров для входа в любую современную среду, предоставляя все необходимые функции для высокопроизводительных веб-приложений.

Vue.js Домашняя страница

В этом руководстве рассматриваются две основные сборки: веб-приложение внешнего интерфейса и серверный REST API-сервер. Интерфейсом будет одностраничное приложение (SPA) с домашней страницей, входом и выходом из системы, а также менеджером сообщений.

OpenID Connect (OIDC) Okta будет обрабатывать аутентификацию нашего веб-приложения с помощью Vue SDK Okta . Если неаутентифицированный пользователь переходит к менеджеру сообщений, веб-приложение должно попытаться аутентифицировать пользователя.

Сервер будет работать Express с Sequelize и Epilogue . На высоком уровне, с помощью Sequelize и Epilogue вы можете быстро генерировать динамические конечные точки REST всего за несколько строк кода.

Вы будете использовать аутентификацию на основе JWT при отправке запросов от веб-приложения и JWT Verifier от Okta в промежуточном программном обеспечении Express для проверки токена. Ваше приложение будет предоставлять следующие конечные точки, для которых все запросы должны иметь действительный токен доступа.

- GET /posts - GET /posts/:id - POST /posts - PUT /posts/:id - DELETE /posts/:id 

Создайте приложение Vue.js

Чтобы быстро начать работу над своим проектом, вы можете использовать функциональность лесов из vue-cli . В этом руководстве вы собираетесь использовать шаблон прогрессивного веб-приложения (PWA), который включает в себя несколько функций, включая веб-пакет , горячую перезагрузку , извлечение CSS и модульное тестирование.

Если вы не знакомы с принципами PWA, ознакомьтесь с нашим полным руководством по прогрессивным веб-приложениям .

Для установки vue-cli запустите:

 npm install -g vue-cli 

Далее вам нужно инициализировать ваш проект. При запуске команды vue init просто примите все значения по умолчанию.

 vue init pwa my-vue-app cd ./my-vue-app npm install npm run dev 

Направьте ваш любимый браузер на http://localhost:8080 и вы должны увидеть плоды своего труда:

Добро пожаловать в Ваш Vue.js PWA

Дополнительные кредиты : Проверьте другие шаблоны, доступные для vue-cli .

Установить Bootstrap

Давайте установим bootstrap-vue, чтобы вы могли использовать различные готовые компоненты (плюс вы можете сосредоточиться на функциональности, а не на собственном CSS):

 npm i --save bootstrap-vue bootstrap 

Чтобы завершить установку, измените ./src/main.js чтобы включить bootstrap-vue и импортировать необходимые файлы CSS. Ваш файл ./src/main.js должен выглядеть так:

 // The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' import router from './router' import BootstrapVue from 'bootstrap-vue' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' Vue.use(BootstrapVue) Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, template: '<App/>', components: { App } }) 

Добавить аутентификацию с Okta

Работа с аутентификацией в веб-приложении — проклятие существования каждого разработчика. Вот тут и приходит Okta, чтобы обезопасить ваши веб-приложения с минимальным кодом. Для начала вам нужно будет создать приложение OIDC в ​​Okta. Подпишитесь на вечно бесплатную учетную запись разработчика (или войдите, если у вас уже есть)

Okta Developer Зарегистрироваться

После входа в систему создайте новое приложение, нажав «Добавить приложение».

Добавить приложение

Выберите опцию «Одностраничное приложение».

Новые параметры приложения

Настройки приложения по умолчанию должны быть такими же, как на картинке.

Настройки приложения Okta

Чтобы установить Okta Vue SDK, выполните следующую команду:

 npm i --save @okta/okta-vue 

Откройте ./src/router/index.js и замените весь файл следующим кодом.

 import Vue from 'vue' import Router from 'vue-router' import Hello from '@/components/Hello' import PostsManager from '@/components/PostsManager' import Auth from '@okta/okta-vue' Vue.use(Auth, { issuer: 'https://{yourOktaDomain}.com/oauth2/default', client_id: '{yourClientId}', redirect_uri: 'http://localhost:8080/implicit/callback', scope: 'openid profile email' }) Vue.use(Router) let router = new Router({ mode: 'history', routes: [ { path: '/', name: 'Hello', component: Hello }, { path: '/implicit/callback', component: Auth.handleCallback() }, { path: '/posts-manager', name: 'PostsManager', component: PostsManager, meta: { requiresAuth: true } } ] }) router.beforeEach(Vue.prototype.$auth.authRedirectGuard()) export default router 

Вам нужно заменить {yourOktaDomain} и {yourClientId} которые можно найти на странице обзора вашего приложения в консоли разработчика Okta. Это authClient объект authClient в ваш экземпляр Vue, к которому можно обратиться, вызвав this.$auth любом месте вашего экземпляра Vue.

 Vue.use(Auth, { issuer: 'https://{yourOktaDomain}.com/oauth2/default', client_id: '{yourClientId}', redirect_uri: 'http://localhost:8080/implicit/callback', scope: 'openid profile email' }) 

Последний шаг процесса аутентификации Okta — перенаправление пользователя обратно в ваше приложение со значениями токенов в URL. Компонент Auth.handleCallback() включенный в SDK, обрабатывает перенаправление и сохраняет токены в браузере.

 { path: '/implicit/callback', component: Auth.handleCallback() } 

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

SDK поставляется с методом auth.authRedirectGuard() который проверяет метаданные совпадающих маршрутов для ключа auth.authRedirectGuard() и перенаправляет пользователя в поток аутентификации, если они не аутентифицированы.

 router.beforeEach(Vue.prototype.$auth.authRedirectGuard()) 

С этим установленным средством навигации любой маршрут, имеющий следующие метаданные, будет защищен.

 meta: { requiresAuth: true } 

Настройте макет приложения в Vue

Макет веб-приложения находится в компоненте ./src/App.vue . Вы можете использовать компонент router-view, чтобы отобразить соответствующий компонент для данного пути.

В главном меню вы захотите изменить видимость определенных пунктов меню в зависимости от статуса activeUser :

  • Не аутентифицирован: Показать только Логин
  • Аутентифицировано: Показать только Выход

Вы можете переключать видимость этих пунктов меню, используя директиву v-if в Vue.js, которая проверяет существование activeUser на компоненте. Когда компонент загружен (который вызывает activeUser created() ) или когда меняется маршрут, мы хотим обновить activeUser .

Откройте ./src/App.vue и скопируйте / вставьте следующий код.

 <template> <div id="app"> <b-navbar toggleable="md" type="dark" variant="dark"> <b-navbar-toggle target="nav_collapse"></b-navbar-toggle> <b-navbar-brand to="/">My Vue App</b-navbar-brand> <b-collapse is-nav id="nav_collapse"> <b-navbar-nav> <b-nav-item to="/">Home</b-nav-item> <b-nav-item to="/posts-manager">Posts Manager</b-nav-item> <b-nav-item href="#" @click.prevent="login" v-if="!activeUser">Login</b-nav-item> <b-nav-item href="#" @click.prevent="logout" v-else>Logout</b-nav-item> </b-navbar-nav> </b-collapse> </b-navbar> <!-- routes will be rendered here --> <router-view /> </div> </template> <script> export default { name: 'app', data () { return { activeUser: null } }, async created () { await this.refreshActiveUser() }, watch: { // everytime a route is changed refresh the activeUser '$route': 'refreshActiveUser' }, methods: { login () { this.$auth.loginRedirect() }, async refreshActiveUser () { this.activeUser = await this.$auth.getUser() }, async logout () { await this.$auth.logout() await this.refreshActiveUser() this.$router.push('/') } } } </script> 

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

 async logout () { await this.$auth.logout() await this.refreshActiveUser() this.$router.push('/') } 

Компоненты являются строительными блоками в Vue.js. Каждая из ваших страниц будет определена в приложении как компонент. Поскольку в шаблоне веб-пакета vue-cli используется vue-loader , исходные файлы компонентов имеют соглашение, которое разделяет шаблон, сценарий и стиль ( см. Здесь ).

Теперь, когда вы добавили vue-bootstrap, измените ./src/components/Hello.vue чтобы удалить ./src/components/Hello.vue ссылок, которые генерирует vue-cli.

 <template> <div class="hero"> <div> <h1 class="display-3">Hello World</h1> <p class="lead">This is the homepage of your vue app</p> </div> </div> </template> <style> .hero { height: 90vh; display: flex; align-items: center; justify-content: center; text-align: center; } .hero .lead { font-weight: 200; font-size: 1.5rem; } </style> 

На этом этапе вы можете заглушить страницу Post Manager, чтобы проверить ваш процесс аутентификации. Как только вы подтвердите аутентификацию, вы начнете создавать вызовы API и компоненты, необходимые для выполнения операций CRUD в вашей модели сообщений.

Создайте новый файл ./src/components/PostsManager.vue и вставьте следующий код:

 <template> <div class="container-fluid mt-4"> <h1 class="h1">Posts Manager</h1> <p>Only authenticated users should see this page</p> </div> </template> 

Возьмите свой интерфейс Vue.js и потоки аутентификации для тест-драйва

В вашем терминале запустите npm run dev (если он еще не запущен). Перейдите по http://localhost:8080 и вы должны увидеть новую домашнюю страницу.

Привет, мир

Если вы нажмете Менеджер постов или Логин, вы будете перенаправлены в поток Okta. Введите свои учетные данные в учетной записи Okta dev.

ПРИМЕЧАНИЕ. Если вы вошли в свою учетную запись Okta Developer, вы будете автоматически перенаправлены обратно в приложение. Вы можете проверить это, используя режим инкогнито или приватный просмотр.

Okta Sign-In

В случае успеха вы должны вернуться на домашнюю страницу, на которой вы авторизованы

Домашняя страница после входа в систему

При нажатии на ссылку « Менеджер сообщений» должен отображаться защищенный компонент

Менеджер сообщений

Добавить Backend REST API Server

Теперь, когда пользователи могут безопасно проходить аутентификацию, вы можете создать сервер API REST для выполнения операций CRUD на пост-модели. Добавьте следующие зависимости в ваш проект:

 npm i --save express cors @okta/jwt-verifier sequelize sqlite3 epilogue axios 

Затем создайте файл ./src/server.js и вставьте следующий код.

 const express = require('express') const cors = require('cors') const bodyParser = require('body-parser') const Sequelize = require('sequelize') const epilogue = require('epilogue') const OktaJwtVerifier = require('@okta/jwt-verifier') const oktaJwtVerifier = new OktaJwtVerifier({ clientId: '{yourClientId}', issuer: 'https://{yourOktaDomain}.com/oauth2/default' }) let app = express() app.use(cors()) app.use(bodyParser.json()) // verify JWT token middleware app.use((req, res, next) => { // require every request to have an authorization header if (!req.headers.authorization) { return next(new Error('Authorization header is required')) } let parts = req.headers.authorization.trim().split(' ') let accessToken = parts.pop() oktaJwtVerifier.verifyAccessToken(accessToken) .then(jwt => { req.user = { uid: jwt.claims.uid, email: jwt.claims.sub } next() }) .catch(next) // jwt did not verify! }) // For ease of this tutorial, we are going to use SQLite to limit dependencies let database = new Sequelize({ dialect: 'sqlite', storage: './test.sqlite' }) // Define our Post model // id, createdAt, and updatedAt are added by sequelize automatically let Post = database.define('posts', { title: Sequelize.STRING, body: Sequelize.TEXT }) // Initialize epilogue epilogue.initialize({ app: app, sequelize: database }) // Create the dynamic REST resource for our Post model let userResource = epilogue.resource({ model: Post, endpoints: ['/posts', '/posts/:id'] }) // Resets the database and launches the express app on :8081 database .sync({ force: true }) .then(() => { app.listen(8081, () => { console.log('listening to port localhost:8081') }) }) 

Обязательно замените переменные {yourOktaDomain} и {clientId} в приведенном выше коде значениями из вашего приложения OIDC в ​​Okta.

Добавить Sequelize

Sequelize — это основанная на обещаниях ORM для Node.js. Он поддерживает диалекты PostgreSQL, MySQL, SQLite и MSSQL и обеспечивает надежную поддержку транзакций, отношения, репликацию чтения и многое другое.

Для простоты этого руководства вы будете использовать SQLite для ограничения внешних зависимостей. Следующий код инициализирует экземпляр Sequelize, используя SQLite в качестве драйвера.

 let database = new Sequelize({ dialect: 'sqlite', storage: './test.sqlite' }) 

Каждый пост имеет title и body . (Поля, createdAt и updatedAt , добавляются Sequelize автоматически). С Sequelize вы определяете модели, вызывая define() для вашего экземпляра.

 let Post = database.define('posts', { title: Sequelize.STRING, body: Sequelize.TEXT }) 

Добавить эпилог

Epilogue создает гибкие конечные точки REST из моделей Sequelize в приложении Express. Если вы когда-либо кодировали конечные точки REST, вы знаете, сколько существует повторений. СУХОЙ FTW!

 // Initialize epilogue epilogue.initialize({ app: app, sequelize: database }) // Create the dynamic REST resource for our Post model let userResource = epilogue.resource({ model: Post, endpoints: ['/posts', '/posts/:id'] }) 

Проверьте свой JWT

Это наиболее важный компонент вашего сервера API REST. Без этого промежуточного программного обеспечения любой пользователь может выполнять операции CRUD с нашей базой данных. Если заголовок авторизации отсутствует или токен доступа недействителен, вызов API завершится ошибкой и вернет ошибку.

 // verify JWT token middleware app.use((req, res, next) => { // require every request to have an authorization header if (!req.headers.authorization) { return next(new Error('Authorization header is required')) } let parts = req.headers.authorization.trim().split(' ') let accessToken = parts.pop() oktaJwtVerifier.verifyAccessToken(accessToken) .then(jwt => { req.user = { uid: jwt.claims.uid, email: jwt.claims.sub } next() }) .catch(next) // jwt did not verify! }) 

Запустите сервер

Откройте новое окно терминала и запустите сервер с помощью командного node ./src/server . Вы должны увидеть отладочную информацию из Sequelize и приложения, прослушивающего порт 8081.

Заполните компонент «Менеджер сообщений»

Теперь, когда сервер REST API завершен, вы можете начать подключать свой менеджер сообщений, чтобы получать сообщения, создавать сообщения, редактировать сообщения и удалять сообщения.

Я всегда централизую свои API-интеграции в единый вспомогательный модуль. Это делает код в компонентах намного чище и обеспечивает единое местоположение на случай, если вам нужно что-то изменить с помощью запроса API.

Создайте файл ./src/api.js и скопируйте / вставьте в него следующий код:

 import Vue from 'vue' import axios from 'axios' const client = axios.create({ baseURL: 'http://localhost:8081/', json: true }) export default { async execute (method, resource, data) { // inject the accessToken for each request let accessToken = await Vue.prototype.$auth.getAccessToken() return client({ method, url: resource, data, headers: { Authorization: `Bearer ${accessToken}` } }).then(req => { return req.data }) }, getPosts () { return this.execute('get', '/posts') }, getPost (id) { return this.execute('get', `/posts/${id}`) }, createPost (data) { return this.execute('post', '/posts', data) }, updatePost (id, data) { return this.execute('put', `/posts/${id}`, data) }, deletePost (id) { return this.execute('delete', `/posts/${id}`) } } 

Когда вы аутентифицируетесь с помощью OIDC, маркер доступа сохраняется локально в браузере. Поскольку у каждого запроса API должен быть токен доступа, вы можете извлечь его из клиента аутентификации и установить в запросе.

 let accessToken = await Vue.prototype.$auth.getAccessToken() return client({ method, url: resource, data, headers: { Authorization: `Bearer ${accessToken}` } }) 

Создавая следующие прокси-методы внутри вашего помощника API, код вне вспомогательного модуля остается чистым и семантическим.

 getPosts () { return this.execute('get', '/posts') }, getPost (id) { return this.execute('get', `/posts/${id}`) }, createPost (data) { return this.execute('post', '/posts', data) }, updatePost (id, data) { return this.execute('put', `/posts/${id}`, data) }, deletePost (id) { return this.execute('delete', `/posts/${id}`) } 

Теперь у вас есть все компоненты, необходимые для подключения компонента менеджера постов для выполнения операций CRUD через REST API. Откройте ./src/components/PostsManager.vue и скопируйте / вставьте следующий код.

 <template> <div class="container-fluid mt-4"> <h1 class="h1">Posts Manager</h1> <b-alert :show="loading" variant="info">Loading...</b-alert> <b-row> <b-col> <table class="table table-striped"> <thead> <tr> <th>ID</th> <th>Title</th> <th>Updated At</th> <th>&nbsp;</th> </tr> </thead> <tbody> <tr v-for="post in posts" :key="post.id"> <td>{{ post.id }}</td> <td>{{ post.title }}</td> <td>{{ post.updatedAt }}</td> <td class="text-right"> <a href="#" @click.prevent="populatePostToEdit(post)">Edit</a> - <a href="#" @click.prevent="deletePost(post.id)">Delete</a> </td> </tr> </tbody> </table> </b-col> <b-col lg="3"> <b-card :title="(model.id ? 'Edit Post ID#' + model.id : 'New Post')"> <form @submit.prevent="savePost"> <b-form-group label="Title"> <b-form-input type="text" v-model="model.title"></b-form-input> </b-form-group> <b-form-group label="Body"> <b-form-textarea rows="4" v-model="model.body"></b-form-textarea> </b-form-group> <div> <b-btn type="submit" variant="success">Save Post</b-btn> </div> </form> </b-card> </b-col> </b-row> </div> </template> <script> import api from '@/api' export default { data () { return { loading: false, posts: [], model: {} } }, async created () { this.refreshPosts() }, methods: { async refreshPosts () { this.loading = true this.posts = await api.getPosts() this.loading = false }, async populatePostToEdit (post) { this.model = Object.assign({}, post) }, async savePost () { if (this.model.id) { await api.updatePost(this.model.id, this.model) } else { await api.createPost(this.model) } this.model = {} // reset form await this.refreshPosts() }, async deletePost (id) { if (confirm('Are you sure you want to delete this post?')) { // if we are editing a post we deleted, remove it from the form if (this.model.id === id) { this.model = {} } await api.deletePost(id) await this.refreshPosts() } } } } </script> 

Список сообщений

Вы будете использовать api.getPosts() для получения сообщений с вашего сервера API REST. Вам следует обновить список сообщений, когда компонент загружен и после любой операции мутации (создание, обновление или удаление).

 async refreshPosts () { this.loading = true this.posts = await api.getPosts() this.loading = false } 

Атрибут this.loading переключается, чтобы пользовательский интерфейс мог отражать ожидающий вызов API. Вы можете не увидеть сообщение о загрузке, поскольку запрос API не отправляется в Интернет.

Создание сообщений

Форма включена в компонент, чтобы сохранить сообщение. Он подключен к вызову savePosts() когда форма отправлена, а ее входные данные привязаны к объекту model в компоненте.

Когда savePost() , он выполняет обновление или создание на основе существования model.id . В основном это ярлык, чтобы не создавать две отдельные формы для создания и обновления.

 async savePost () { if (this.model.id) { await api.updatePost(this.model.id, this.model) } else { await api.createPost(this.model) } this.model = {} // reset form await this.refreshPosts() } 

Обновление сообщений

При обновлении сообщения вы должны сначала загрузить сообщение в форму. Это устанавливает model.id который будет запускать обновление в savePost() .

 async populatePostToEdit (post) { this.model = Object.assign({}, post) } 

Важно: Object.assign() копирует значение аргумента post, а не ссылку. При работе с мутациями объектов в Vue всегда следует указывать значение, а не ссылку.

Удаление сообщений

Чтобы удалить сообщение просто позвоните api.deletePost(id) . Перед удалением всегда полезно подтвердить, так что давайте добавим окно с собственным подтверждением, чтобы убедиться, что щелчок был преднамеренным.

 async deletePost (id) { if (confirm('Are you sure you want to delete this post?')) { await api.deletePost(id) await this.refreshPosts() } } 

Проверьте свое приложение CRU Vue.js + Node

Убедитесь, что сервер и интерфейс работают.

Терминал 1

 node ./src/server 

Терминал 2

 npm run dev 

Перейдите по http://localhost:8080 и поверните его.

Новый пост

Новая Hello World Post

Удалить сообщение

Делай больше с Vue!

Как я уже говорил в начале этого поста, я думаю, что Vue стоит на голову выше других рамок. Вот пять быстрых причин, почему:

Я рассмотрел много материала в этом уроке, но не расстраивайтесь, если вы не поняли все с первого раза. Чем больше вы работаете с этими технологиями, тем более знакомыми они станут.

Чтобы узнать больше о Vue.js, зайдите на https://vuejs.org или ознакомьтесь с другими полезными ресурсами команды @oktadev :

Вы можете найти исходный код приложения, разработанного в этом посте, по адресу https://github.com/oktadeveloper/okta-vue-node-example .

Как всегда, следите за @oktadev в Твиттере, чтобы увидеть весь классный контент, который создает наша команда разработчиков.