使用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构建安全的用户管理的主要内容,如果未能解决你的问题,请参考以下文章