Vue.js 中的身份验证

已发表: 2022-03-10
快速总结↬每个处理用户特定数据的 Web 应用程序都需要实现身份验证。 知道如何做到这一点对于 Vue 开发人员来说很重要,这也是本文的重点。 本教程将证明对想要了解 Vue 中的身份验证的初学者开发人员很有用。 为了能够跟进,您需要对 Vue 和 Vuex 有很好的了解。

身份验证对于存储用户数据的应用程序来说是一项非常必要的功能。 这是一个验证用户身份的过程,确保未经授权的用户无法访问私人数据——属于其他用户的数据。 这导致具有只能由经过身份验证的用户访问的受限路由。 这些经过身份验证的用户通过使用他们的登录详细信息(即用户名/电子邮件和密码)进行验证,并为他们分配一个用于访问应用程序受保护资源的令牌。

在本文中,您将学习:

  1. 使用 Axios 进行 Vuex 配置
  2. 定义路线
  3. 处理用户
  4. 处理过期令牌

依赖项

我们将使用以下有助于身份验证的依赖项:

  • 爱讯
    用于从我们的 API 发送和检索数据
  • Vuex
    用于存储从我们的 API 获得的数据
  • Vue路由器
    用于路线的导航和保护

我们将使用这些工具,看看它们如何协同工作,为我们的应用程序提供强大的身份验证功能。

后端 API

我们将构建一个简单的博客站点,它将使用这个 API。 您可以查看文档以查看端点以及应如何发送请求。

从文档中,您会注意到很少有端点带有锁。 这是一种表明只有授权用户才能向这些端点发送请求的方式。 不受限制的端点是/register/login端点。 当未经身份验证的用户尝试访问受限端点时,应返回状态代码401的错误。

成功登录用户后,Vue 应用程序将收到访问令牌和一些数据,这些数据将用于设置 cookie 并附加在请求标头中以用于将来的请求。 每次向受限端点发出请求时,后端都会检查请求标头。 不要试图将访问令牌存储在本地存储中。

(大预览)
跳跃后更多! 继续往下看↓

脚手架工程

使用 Vue CLI,运行以下命令来生成应用程序:

 vue create auth-project

导航到您的新文件夹:

 cd auth-project

添加 vue-router 并安装更多依赖项——vuex 和 axios:

 vue add router npm install vuex axios

现在运行你的项目,你应该在浏览器上看到我下面的内容:

 npm run serve

1. 使用 Axios 进行 Vuex 配置

Axios 是一个 JavaScript 库,用于将请求从浏览器发送到 API。 根据 Vuex 文档;

“Vuex 是 Vue.js 应用程序的状态管理模式 + 库。 它充当应用程序中所有组件的集中存储,其规则确保状态只能以可预测的方式发生变化。”

那是什么意思? Vuex 是 Vue 应用程序中使用的存储,它允许我们保存可供每个组件使用的数据,并提供更改这些数据的方法。 我们将在 Vuex 中使用 Axios 来发送我们的请求并对我们的状态(数据)进行更改。 Axios 将在 Vuex actions中用于发送GETPOST ,获得的响应将用于向mutations发送信息并更新我们的存储数据。

为了处理刷新后的 Vuex 重置,我们将使用vuex-persistedstate ,这是一个在页面重新加载之间保存我们的 Vuex 数据的库。

 npm install --save vuex-persistedstate

现在让我们在src中创建一个新的文件夹store ,用于配置 Vuex 存储。 在store文件夹中,新建一个文件夹; modules和文件index.js 。 请务必注意,只有在文件夹没有自动为您创建时,您才需要执行此操作。

 import Vuex from 'vuex'; import Vue from 'vue'; import createPersistedState from "vuex-persistedstate"; import auth from './modules/auth'; // Load Vuex Vue.use(Vuex); // Create store export default new Vuex.Store({ modules: { auth }, plugins: [createPersistedState()] });

在这里,我们使用Vuex并将modules文件夹中的身份验证module导入我们的商店。

模块

模块是我们商店的不同部分,它们一起处理类似的任务,包括:

  • 状态
  • 行动
  • 突变
  • 吸气剂

在我们继续之前,让我们编辑我们的main.js文件。

 import Vue from 'vue' import App from './App.vue' import router from './router'; import store from './store'; import axios from 'axios'; axios.defaults.withCredentials = true axios.defaults.baseURL = 'https://gabbyblog.herokuapp.com/'; Vue.config.productionTip = false new Vue({ store, router, render: h => h(App) }).$mount('#app')

我们从./store文件夹以及 Axios 包中导入了store对象。

如前所述,需要在请求标头中设置从 API 获取的访问令牌 cookie 和其他必要数据,以供将来请求使用。 由于我们将在发出请求时使用 Axios,因此我们需要配置 Axios 以使用它。 在上面的代码片段中,我们使用axios.defaults.withCredentials = true来执行此操作,这是必需的,因为默认情况下,Axios 不会传递 cookie。

aaxios.defaults.withCredentials = true指示 Axios 发送所有带有凭据的请求,例如; 授权标头、TLS 客户端证书或 cookie(在我们的例子中)。

我们为我们的API的 Axios 请求设置axios.defaults.baseURL这样,每当我们通过 Axios 发送时,它都会使用这个基本 URL。 这样,我们就可以只将/register/login等端点添加到我们的操作中,而无需每次都说明完整的 URL。

现在在storemodules文件夹中创建一个名为auth.js的文件

//store/modules/auth.js import axios from 'axios'; const state = { }; const getters = { }; const actions = { }; const mutations = { }; export default { state, getters, actions, mutations };

state

在我们的state字典中,我们将定义我们的数据及其默认值:

 const state = { user: null, posts: null, };

我们正在设置state的默认值,它是一个包含userposts的对象,它们的初始值为null

行动

动作是用于commit突变以更改状态或可用于dispatch (即调用另一个动作)的函数。 它可以在不同的组件或视图中调用,然后提交我们的状态突变;

注册动作

我们的Register操作接收表单数据,将数据发送到我们的/register端点,并将响应分配给变量response 。 接下来,我们将发送表单usernamepasswordlogin操作。 这样,我们在用户注册后登录,因此他们被重定向到/posts页面。

 async Register({dispatch}, form) { await axios.post('register', form) let UserForm = new FormData() UserForm.append('username', form.username) UserForm.append('password', form.password) await dispatch('LogIn', UserForm) },

登录操作

这是主要身份验证发生的地方。 当用户填写他们的用户名和密码时,它被传递给作为 FormData 对象的UserLogIn函数获取User对象并向/login端点发出POST请求以登录用户。

Login函数最终将username提交给setUser突变。

 async LogIn({commit}, User) { await axios.post('login', User) await commit('setUser', User.get('username')) },

创建发布操作

我们的CreatePost动作是一个函数,它接收post并将其发送到我们的/post端点,然后调度GetPosts动作。 这使用户能够在创建后查看他们的帖子。

 async CreatePost({dispatch}, post) { await axios.post('post', post) await dispatch('GetPosts') },

获取帖子操作

我们的GetPosts操作向我们的/posts端点发送一个GET请求,以获取我们 API 中的帖子并提交setPosts突变。

 async GetPosts({ commit }){ let response = await axios.get('posts') commit('setPosts', response.data) },

注销操作

async LogOut({commit}){ let user = null commit('logout', user) }

我们的LogOut操作将我们的user从浏览器缓存中删除。 它通过提交logout来做到这一点:

突变

const mutations = { setUser(state, username){ state.user = username }, setPosts(state, posts){ state.posts = posts }, LogOut(state){ state.user = null state.posts = null }, };

除了Logout之外,每个突变都从提交它的操作中获取state和值。 获取的值用于更改某些部分或全部或类似在LogOut中将所有变量设置回 null。

吸气剂

Getter 是获取状态的函数。 它可以在多个组件中使用以获取当前状态。 isAuthenticatated函数检查state.user是否已定义或为 null,并分别返回truefalseStatePostsStateUser分别返回state.postsstate.user值。

 const getters = { isAuthenticated: state => !!state.user, StatePosts: state => state.posts, StateUser: state => state.user, };

现在您的整个auth.js文件应该类似于我在 GitHub 上的代码。

设置组件

1. NavBar.vueApp.vue组件

在您的src/components文件夹中,删除HelloWorld.vue和一个名为NavBar.vue的新文件。

这是我们导航栏的组件,它链接到我们组件的不同页面被路由到这里。 每个路由器链接都指向我们应用程序上的一个路由/页面。

v-if="isLoggedIn"是在用户登录时显示Logout链接并隐藏RegisterLogin路由的条件。 我们有一个logout方法,它只能被登录用户访问,当点击Logout链接时,它会被调用。 它将调度LogOut操作,然后将用户引导到登录页面。

 <template> <div> <router-link to="/">Home</router-link> | <router-link to="/posts">Posts</router-link> | <span v-if="isLoggedIn"> <a @click="logout">Logout</a> </span> <span v-else> <router-link to="/register">Register</router-link> | <router-link to="/login">Login</router-link> </span> </div> </template> <script> export default { name: 'NavBar', computed : { isLoggedIn : function(){ return this.$store.getters.isAuthenticated} }, methods: { async logout (){ await this.$store.dispatch('LogOut') this.$router.push('/login') } }, } </script> <style> #nav { padding: 30px; } #nav a { font-weight: bold; color: #2c3e50; } a:hover { cursor: pointer; } #nav a.router-link-exact-active { color: #42b983; } </style>

现在编辑你的App.vue组件,如下所示:

 <template> <div> <NavBar /> <router-view/> </div> </template> <script> // @ is an alias to /src import NavBar from '@/components/NavBar.vue' export default { components: { NavBar } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; } </style>

在这里,我们导入了我们在上面创建的 NavBar 组件,并将其放置在<router-view />之前的模板部分。

2. 视图组件

视图组件是应用程序上的不同页面,它们将在路由下定义并且可以从导航栏访问。 开始 转到views文件夹,删除About.vue组件,并添加以下组件:

  • Home.vue
  • Register.vue
  • Login.vue
  • Posts.vue

Home.vue

Home.vue重写为如下所示:

 <template> <div class="home"> <p>Heyyyyyy welcome to our blog, check out our posts</p> </div> </template> <script> export default { name: 'Home', components: { } } </script>

这将在用户访问主页时向用户显示欢迎文本。

Register.vue

这是我们希望我们的用户能够在我们的应用程序上注册的页面。 当用户填写表格时,他们的信息被发送到 API 并添加到数据库中,然后登录。

查看 API, /register端点需要我们用户的用户usernamefull_name名和password 。 现在让我们创建一个页面和一个表单来获取这些信息:

 <template> <div class="register"> <div> <form @submit.prevent="submit"> <div> <label for="username">Username:</label> <input type="text" name="username" v-model="form.username"> </div> <div> <label for="full_name">Full Name:</label> <input type="text" name="full_name" v-model="form.full_name"> </div> <div> <label for="password">Password:</label> <input type="password" name="password" v-model="form.password"> </div> <button type="submit"> Submit</button> </form> </div> <p v-if="showError">Username already exists</p> </div> </template>

Register组件中,我们需要调用Register操作来接收表单数据。

 <script> import { mapActions } from "vuex"; export default { name: "Register", components: {}, data() { return { form: { username: "", full_name: "", password: "", }, showError: false }; }, methods: { ...mapActions(["Register"]), async submit() { try { await this.Register(this.form); this.$router.push("/posts"); this.showError = false } catch (error) { this.showError = true } }, }, }; </script>

我们首先从 Vuex 导入mapActions ,它的作用是将操作从我们的 store 导入到组件中。 这允许我们从组件调用操作。

data()包含将在此组件中使用的本地状态值,我们有一个包含usernamefull_namepasswordform对象,它们的初始值设置为空字符串。 我们还有showError是一个布尔值,用于显示或不显示错误。

methods中,我们使用MapactionsRegister动作导入到组件中,因此可以使用this.Register调用Register动作。

我们有一个提交方法,它调用我们可以使用this.Register访问的Register操作,并将其发送this.form 。 如果没有遇到error ,我们使用this.$router将用户发送到登录页面。 否则我们将showError设置为 true。

完成后,我们可以包含一些样式。

 <style scoped> * { box-sizing: border-box; } label { padding: 12px 12px 12px 0; display: inline-block; } button[type=submit] { background-color: #4CAF50; color: white; padding: 12px 20px; cursor: pointer; border-radius:30px; } button[type=submit]:hover { background-color: #45a049; } input { margin: 5px; box-shadow:0 0 15px 4px rgba(0,0,0,0.06); padding:10px; border-radius:30px; } #error { color: red; } </style>

Login.vue

我们的登录页面是注册用户,将输入他们的usernamepassword以通过 API 进行身份验证并登录到我们的网站。

 <template> <div class="login"> <div> <form @submit.prevent="submit"> <div> <label for="username">Username:</label> <input type="text" name="username" v-model="form.username" /> </div> <div> <label for="password">Password:</label> <input type="password" name="password" v-model="form.password" /> </div> <button type="submit">Submit</button> </form> <p v-if="showError">Username or Password is incorrect</p> </div> </div> </template>

现在我们必须将表单数据传递给发送请求的操作,然后将它们推送到安全页面Posts

 <script> import { mapActions } from "vuex"; export default { name: "Login", components: {}, data() { return { form: { username: "", password: "", }, showError: false }; }, methods: { ...mapActions(["LogIn"]), async submit() { const User = new FormData(); User.append("username", this.form.username); User.append("password", this.form.password); try { await this.LogIn(User); this.$router.push("/posts"); this.showError = false } catch (error) { this.showError = true } }, }, }; </script>

我们导入Mapactions并将其用于将LogIn操作导入到组件中,这将在我们的submit函数中使用。

Login操作之后,用户被重定向到/posts页面。 如果发生错误,将捕获错误并将ShowError设置为 true。

现在,一些样式:

 <style scoped> * { box-sizing: border-box; } label { padding: 12px 12px 12px 0; display: inline-block; } button[type=submit] { background-color: #4CAF50; color: white; padding: 12px 20px; cursor: pointer; border-radius:30px; } button[type=submit]:hover { background-color: #45a049; } input { margin: 5px; box-shadow:0 0 15px 4px rgba(0,0,0,0.06); padding:10px; border-radius:30px; } #error { color: red; } </style>

Posts.vue

我们的帖子页面是仅对经过身份验证的用户可用的安全页面。 在此页面上,他们可以访问 API 数据库中的帖子。 这允许用户访问帖子,也使他们能够向 API 创建帖子。

 <template> <div class="posts"> <div v-if="User"> <p>Hi {{User}}</p> </div> <div> <form @submit.prevent="submit"> <div> <label for="title">Title:</label> <input type="text" name="title" v-model="form.title"> </div> <div> <textarea name="write_up" v-model="form.write_up" placeholder="Write up..."></textarea> </div> <button type="submit"> Submit</button> </form> </div> <div class="posts" v-if="Posts"> <ul> <li v-for="post in Posts" :key="post.id"> <div> <p>{{post.title}}</p> <p>{{post.write_up}}</p> <p>Written By: {{post.author.username}}</p> </div> </li> </ul> </div> <div v-else> Oh no!!! We have no posts </div> </div> </template>

在上面的代码中,我们有一个表单供用户创建新帖子。 提交表单应该会导致帖子被发送到 API——我们将很快添加执行此操作的方法。 我们还有一个部分显示从 API 获得的帖子(如果用户有的话)。 如果用户没有任何帖子,我们只需显示一条消息,即没有帖子。

StateUserStatePosts getter 被映射,即使用mapGetters导入Posts.vue ,然后可以在模板中调用它们。

 <script> import { mapGetters, mapActions } from "vuex"; export default { name: 'Posts', components: { }, data() { return { form: { title: '', write_up: '', } }; }, created: function () { // a function to call getposts action this.GetPosts() }, computed: { ...mapGetters({Posts: "StatePosts", User: "StateUser"}), }, methods: { ...mapActions(["CreatePost", "GetPosts"]), async submit() { try { await this.CreatePost(this.form); } catch (error) { throw "Sorry you can't make a post now!" } }, } }; </script>

我们有一个form的初始状态,它是一个以titlewrite_up为键的对象,其值设置为空字符串。 这些值将更改为用户在我们组件的模板部分中输入的任何内容。

当用户提交帖子时,我们调用this.CreatePost来接收表单对象。

正如您在created的生命周期中看到的那样,我们有this.GetPosts在创建组件时获取帖子。

一些造型,

 <style scoped> * { box-sizing: border-box; } label { padding: 12px 12px 12px 0; display: inline-block; } button[type=submit] { background-color: #4CAF50; color: white; padding: 12px 20px; cursor: pointer; border-radius:30px; margin: 10px; } button[type=submit]:hover { background-color: #45a049; } input { width:60%; margin: 15px; border: 0; box-shadow:0 0 15px 4px rgba(0,0,0,0.06); padding:10px; border-radius:30px; } textarea { width:75%; resize: vertical; padding:15px; border-radius:15px; border:0; box-shadow:0 0 15px 4px rgba(0,0,0,0.06); height:150px; margin: 15px; } ul { list-style: none; } #post-div { border: 3px solid #000; width: 500px; margin: auto; margin-bottom: 5px;; } </style>

2. 定义路线

在我们的router/index.js文件中,导入我们的视图并为每个视图定义路由

import Vue from 'vue' import VueRouter from 'vue-router' import store from '../store'; import Home from '../views/Home.vue' import Register from '../views/Register' import Login from '../views/Login' import Posts from '../views/Posts' Vue.use(VueRouter) const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/register', name: "Register", component: Register, meta: { guest: true }, }, { path: '/login', name: "Login", component: Login, meta: { guest: true }, }, { path: '/posts', name: Posts, component: Posts, meta: {requiresAuth: true}, } ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) export default router

3. 处理用户

  • 未经授权的用户
    如果您注意到在定义我们的帖子路由时,我们添加了一个meta键来指示用户必须经过身份验证,现在我们需要一个router.BeforeEach导航守卫来检查路由是否具有meta: {requiresAuth: true}键。 如果路由具有meta键,它会检查存储中的令牌; 如果存在,它将它们重定向到login路由。
 const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) router.beforeEach((to, from, next) => { if(to.matched.some(record => record.meta.requiresAuth)) { if (store.getters.isAuthenticated) { next() return } next('/login') } else { next() } }) export default router
  • 授权用户
    我们在/register/login路由上也有一个meta数据。 meta: {guest: true}阻止登录的用户使用guest元访问路由。
 router.beforeEach((to, from, next) => { if (to.matched.some((record) => record.meta.guest)) { if (store.getters.isAuthenticated) { next("/posts"); return; } next(); } else { next(); } });

最后,你的文件应该是这样的:

 import Vue from "vue"; import VueRouter from "vue-router"; import store from "../store"; import Home from "../views/Home.vue"; import Register from "../views/Register"; import Login from "../views/Login"; import Posts from "../views/Posts"; Vue.use(VueRouter); const routes = [ { path: "/", name: "Home", component: Home, }, { path: "/register", name: "Register", component: Register, meta: { guest: true }, }, { path: "/login", name: "Login", component: Login, meta: { guest: true }, }, { path: "/posts", name: "Posts", component: Posts, meta: { requiresAuth: true }, }, ]; const router = new VueRouter({ mode: "history", base: process.env.BASE_URL, routes, }); router.beforeEach((to, from, next) => { if (to.matched.some((record) => record.meta.requiresAuth)) { if (store.getters.isAuthenticated) { next(); return; } next("/login"); } else { next(); } }); router.beforeEach((to, from, next) => { if (to.matched.some((record) => record.meta.guest)) { if (store.getters.isAuthenticated) { next("/posts"); return; } next(); } else { next(); } }); export default router;

4.处理过期令牌(禁止请求)

我们的 API 设置为 30 分钟后令牌过期,现在如果我们尝试在 30 分钟后访问posts页面,我们会收到401错误,这意味着我们必须再次登录,所以我们将设置一个拦截器,如果我们得到一个401错误,然后它会将我们重定向回login页面。

main.js文件中的 Axios 默认 URL 声明之后添加以下代码段。

 axios.interceptors.response.use(undefined, function (error) { if (error) { const originalRequest = error.config; if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; store.dispatch('LogOut') return router.push('/login') } } })

这将使您的代码处于与 GitHub 上的示例相同的状态。

结论

如果您一直坚持到最后,那么您现在应该能够构建一个功能齐全且安全的前端应用程序。 现在您已经了解了更多关于 Vuex 以及如何将其与 Axios 集成,以及如何在重新加载后保存其数据。

  • 代码在 GitHub 上可用 →

  • 托管网站: https://nifty-hopper-1e9895.netlify.app/ ://nifty-hopper-1e9895.netlify.app/

  • 接口: https://gabbyblog.herokuapp.com ://gabbyblog.herokuapp.com

  • API 文档: https://gabbyblog.herokuapp.com/docs ://gabbyblog.herokuapp.com/docs

资源

  • “使用 Axios 处理 Cookie”,Aditya Srivastava,Medium
  • “在 Vue 中创建身份验证导航守卫”,Laurie Barth,十英里广场博客
  • “Vuex 入门”,官方指南
  • “使用 Vuex 和 Vue 路由器进行 Vue.js JWT 身份验证”,BezKoder