Vue.js 中的身份验证
已发表: 2022-03-10身份验证对于存储用户数据的应用程序来说是一项非常必要的功能。 这是一个验证用户身份的过程,确保未经授权的用户无法访问私人数据——属于其他用户的数据。 这导致具有只能由经过身份验证的用户访问的受限路由。 这些经过身份验证的用户通过使用他们的登录详细信息(即用户名/电子邮件和密码)进行验证,并为他们分配一个用于访问应用程序受保护资源的令牌。
在本文中,您将学习:
- 使用 Axios 进行 Vuex 配置
- 定义路线
- 处理用户
- 处理过期令牌
依赖项
我们将使用以下有助于身份验证的依赖项:
- 爱讯
用于从我们的 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
中用于发送GET
和POST
,获得的响应将用于向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。
现在在store
的modules
文件夹中创建一个名为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
的默认值,它是一个包含user
和posts
的对象,它们的初始值为null
。
行动
动作是用于commit
突变以更改状态或可用于dispatch
(即调用另一个动作)的函数。 它可以在不同的组件或视图中调用,然后提交我们的状态突变;
注册动作
我们的Register
操作接收表单数据,将数据发送到我们的/register
端点,并将响应分配给变量response
。 接下来,我们将发送表单username
和password
到login
操作。 这样,我们在用户注册后登录,因此他们被重定向到/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 对象的User
, LogIn
函数获取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,并分别返回true
或false
。 StatePosts
和StateUser
分别返回state.posts
和state.user
值。
const getters = { isAuthenticated: state => !!state.user, StatePosts: state => state.posts, StateUser: state => state.user, };
现在您的整个auth.js
文件应该类似于我在 GitHub 上的代码。
设置组件
1. NavBar.vue
和App.vue
组件
在您的src/components
文件夹中,删除HelloWorld.vue
和一个名为NavBar.vue
的新文件。
这是我们导航栏的组件,它链接到我们组件的不同页面被路由到这里。 每个路由器链接都指向我们应用程序上的一个路由/页面。
v-if="isLoggedIn"
是在用户登录时显示Logout
链接并隐藏Register
和Login
路由的条件。 我们有一个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
端点需要我们用户的用户username
、 full_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()
包含将在此组件中使用的本地状态值,我们有一个包含username
、 full_name
和password
的form
对象,它们的初始值设置为空字符串。 我们还有showError
是一个布尔值,用于显示或不显示错误。
在methods
中,我们使用Mapactions
将Register
动作导入到组件中,因此可以使用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
我们的登录页面是注册用户,将输入他们的username
和password
以通过 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 获得的帖子(如果用户有的话)。 如果用户没有任何帖子,我们只需显示一条消息,即没有帖子。
StateUser
和StatePosts
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
的初始状态,它是一个以title
和write_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.comAPI 文档:
https://gabbyblog.herokuapp.com/docs
://gabbyblog.herokuapp.com/docs
资源
- “使用 Axios 处理 Cookie”,Aditya Srivastava,Medium
- “在 Vue 中创建身份验证导航守卫”,Laurie Barth,十英里广场博客
- “Vuex 入门”,官方指南
- “使用 Vuex 和 Vue 路由器进行 Vue.js JWT 身份验证”,BezKoder