使用Vue.jsNode和Okta构建安全的用户管理

Posted 瀚林府

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用Vue.jsNode和Okta构建安全的用户管理相关的知识,希望对你有一定的参考价值。

管理多个javascript框架可能并不陌生,因此我们来看看如何一起使用Vue和Node来创建用户管理系统,逐步介绍如何搭建Vue.js项目,将安全身份验证卸载到Okta的OpenID Connect API(OIDC),锁定受保护的路由以及通过后端REST API服务器执行CRUD操作。

  • 带有vue-cli, vue-router和 Okta Vue SDK的 Vue.js

  • 使用 Express, Okta JWT验证程序, Sequelize和 Epilogue结点

关于Vue.js

涵盖两个主要构建,一个前端Web应用程序和后端REST API服务器。前端将是一个带有主页,登录和注销的单页面应用程序(SPA),以及一个职位经理。

Okta的OpenID Connect(OIDC) 将通过使用Okta的Vue SDK来处理我们的Web应用程序的身份验证 。如果未经身份验证的用户导航到帖子管理器,则该Web应用程序应尝试对用户进行身份验证。

服务器将运行 Express 和 Sequelize 和 Epilogue。在高层次上,使用Sequelize和Epilogue,只需几行代码即可快速生成动态REST端点。

您将 在Express中间件的Web应用程序和Okta的JWT验证程序发出请求时使用基于JWT的身份验证 来验证令牌。

- GET /posts

- GET /posts/:id

- POST /posts

- PUT /posts/:id

- DELETE /posts/:id

创建您的Vue.js应用程序

为了让您的项目快速起步,您可以利用vue-cli的脚手架功能 。您将使用 包含webpack, 热重新加载,CSS提取和单元测试等少数功能 的 渐进式Web应用程序(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浏览器

安装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添加身份验证

处理Web应用程序中的身份验证是每个开发人员必须。这就是Okta采用最少代码保护您的Web应用程序的地方。要开始,您需要在Okta创建一个OIDC应用程序。 注册一个永远免费的开发者账户。

登录后,点击“添加应用程序”创建一个新的应用程序。

选择“单页面应用程序”平台选项。

默认的应用程序设置应该与图中所示的相同。

要安装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对象注入到您的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() 检查密钥的匹配路由元数据的方法 , requiresAuth如果未通过身份验证,则会将用户重定向到身份验证流程。

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

安装此导航后,具有以下元数据的任何路线都将受到保护。

meta: {

requiresAuth: true

}

在Vue中自定义您的应用布局

Web应用程序的布局位于组件中 ./src/App.vue。您可以使用 路由器视图 组件为给定路径呈现匹配的组件。

对于主菜单,您需要根据以下情况更改某些菜单项的可见性 activeUser:

  • 未验证:仅显示 登录

  • 已通过身份验证:仅显示 注销

您可以使用v-if Vue.js中的指令来切换这些菜单项的可见性,该 指令检查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 webpack模板使用 vue-loader,因此组件源文件有一个将模板,脚本和样式分开的约定。

现在您已添加vue-bootstrap,请修改 ./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页面存档以测试您的身份验证流程。确认身份验证后,您将开始构建在您的帖子模型上执行CRUD操作所需的API调用和组件。

创建一个新文件 ./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前端和Auth流程

在你的终端运行 npm run dev。http://localhost:8080,你应该看到新的主页。

如果你点击发布 管理器 或 登录, 你应该被定向到Okta的流程。输入您的Okta dev帐户凭证。

注意: 如果您登录Okta开发者账户,您将被自动重定向回应用程序。您可以使用隐身或隐私浏览模式进行测试。

点击 文章管理器 链接应该呈现受保护的组件。

添加后端REST API服务器

现在用户可以安全地进行身份验证,您可以构建REST API服务器以在后期模型上执行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}应用在上面的代码值。

添加续集

Sequelize 是Node.js基于承诺的ORM。它支持方言PostgreSQL,mysql,SQLite和MSSQL,并具有坚实的事务支持,关系,读取复制等功能。

为了简化本教程,您将使用SQLite来限制外部依赖关系。以下代码使用SQLite作为驱动程序初始化Sequelize实例。

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 通过Express应用程序中的Sequelize模型创建灵活的REST端点。如果你编码REST端点,你知道有多少重复。

// 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

这是REST API服务器中最重要的组件。没有这个中间件,任何用户都可以在我们的数据库上执行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}`)

}

您现在拥有了连接您的发布者管理器组件以通过REST API进行CRUD操作所需的所有组件。打开 ./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> </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() 从REST API服务器获取帖子。您应该在组件加载时以及任何变更操作(创建,更新或删除)后刷新帖子列表。

async refreshPosts () {

this.loading = true

this.posts = await api.getPosts()

this.loading = false

}

该属性 this.loading 被切换,以便UI可以反映未决的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() 通话复制后的说法,而不是参考价值。在处理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()

}

}

测试你的Vue.js + Node CRUD应用程序

确保服务器和前端都在运行。

Terminal #1

node ./src/server

Terminal #2

npm run dev

以上是关于使用Vue.jsNode和Okta构建安全的用户管理的主要内容,如果未能解决你的问题,请参考以下文章

由于连接不安全,Okta 未重定向

Okta登录小部件获取名称和头像

okta oauth2 Spring security 所有受保护的页面重定向到登录

使用 OKTA 的 WSO2 最终用户身份验证

如何使用 nodejs 和 okta 进行应用程序注销

在 JAVA 中通过身份验证后如何获取 okta 用户详细信息/当前会话