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