身份验证(登录!)是许多网站的重要组成部分。让我们看看如何在使用 Vue 的网站上进行身份验证,就像使用任何自定义后端一样。Vue 本身无法执行身份验证,我们需要另一个服务来完成,因此我们将使用另一个服务(Firebase),然后将整个体验整合到 Vue 中。
单页应用程序 (SPA) 的身份验证机制与每次页面重新加载的网站大不相同。您不必使用 Vue 创建 SPA,但本教程中我们将使用 SPA。
以下是计划。我们将构建一个 UI 供用户登录,提交的数据将发送到服务器以检查用户是否存在。如果存在,我们将收到一个令牌。这非常有用,因为它将在整个网站中使用,以检查用户是否仍然登录。如果不存在,用户始终可以注册。换句话说,它可以在许多条件上下文中使用。除此之外,如果我们需要从服务器获取任何需要登录才能获取的信息,则令牌将通过 URL 发送到服务器,以便信息只能发送给登录用户。
本教程的完整演示已 发布在 GitHub 上,供那些熟悉代码阅读的人使用。我们其他人可以阅读文章。启动文件也 在 GitHub 上,这样您就可以在我们的共同编码过程中进行操作。
下载仓库后,您将在终端中运行 npm install
。如果您要完全独立构建此应用程序,则必须安装 Vuex、Vue Router 和 axios。我们还将使用 Firebase 来完成此项目,因此请花点时间设置一个免费帐户并在其中创建一个新项目。

将项目添加到 Firebase 后,转到身份验证部分,设置一个登录方法,我们将使用传统的电子邮件/密码提供程序,这些信息将存储在我们的 Firebase 服务器上。

之后,我们将转到 Firebase Auth REST API 文档 获取我们的注册和登录 API 端点。我们需要一个 API 密钥才能在我们的应用程序中使用这些端点,它可以在 Firebase 项目设置中找到。
Firebase 通过 SDK 提供身份验证,但我们使用 Auth API 来演示通过任何自定义后端服务器进行身份验证。
在我们的启动文件中,我们有以下注册表单。由于我们专注于学习概念,因此我们将内容保持非常简单。
<template>
<div id="signup">
<div class="signup-form">
<form @submit.prevent="onSubmit">
<div class="input">
<label for="email">Mail</label>
<input
type="email"
id="email"
v-model="email">
</div>
<div class="input">
<label for="name">Your Name</label>
<input
type="text"
id="name"
v-model.number="name">
</div>
<div class="input">
<label for="password">Password</label>
<input
type="password"
id="password"
v-model="password">
</div>
<div class="submit">
<button type="submit">Submit</button>
</div>
</form>
</div>
</div>
</template>
如果我们没有使用 SPA,我们会自然地使用 axios 在脚本标签中发送数据,如下所示
axios.post('https://identitytoolkit.googleapis.com/v1/account
s:signUp?key=[API_KEY]', {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res)
})
.catch(error => console.log(error))
}
}
注册和登录
使用 SPA(在本例中使用 Vue)与上述方法非常不同。相反,我们将使用 Vuex 在 store.js
文件中的操作中发送授权请求。我们之所以这样做,是因为我们希望整个应用程序都知道用户身份验证状态的任何更改。
actions: {
signup ({commit}, authData) {
axios.post('https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=[API_KEY]', {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res)
router.push("/dashboard")
})
.catch(error => console.log(error))
},
login ({commit}, authData) {
axios.post(https://identitytoolkit.googleapis.com/v1/accounts:signIn?key=[API_KEY]', {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res)
router.push("/dashboard")
})
.catch(error => console.log(error))
}
}
我们可以对登录方法使用几乎相同的内容,但使用登录 API 端点。然后,我们从组件中调度注册和登录,到存储中的相应操作。
methods : {
onSubmit () {
const formData = {
email : this.email,
name : this.name,
password : this.password
}
this.$store.dispatch('signup', formData)
}
}
}
formData
包含用户数据。
methods : {
onSubmit () {
const formData = {
email : this.email,
password : this.password
}
this.$store.dispatch('login', {email: formData.email, password: formData.password})
}
}
我们将从注册/登录表单中接收到的身份验证数据(即令牌和用户 ID)作为状态与 Vuex 一起使用。它最初将是 null
。
state: {
idToken: null,
userId: null,
user: null
}
我们现在在变异中创建一个名为 authUser
的新方法,它将存储从响应中收集的数据。我们需要将路由器导入到存储中,因为我们稍后将需要它。
import router from '/router'
mutations : {
authUser (state, userData) {
state.idToken = userData.token
state.userId = userData.userId
}
}
在操作中的注册/登录方法的 .then
块中,我们将响应提交到刚刚创建的 authUser
变异,并保存到本地存储。
actions: {
signup ({commit}, authData) {
axios.post('https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=[API_KEY]'), {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res)
commit('authUser', {
token: res.data.idToken,
userId: res.data.localId
})
localStorage.setItem('token', res.data.idToken)
localStorage.setItem('userId', res.data.localId)
router.push("/dashboard")
})
.catch(error => console.log(error))
},
login ({commit}, authData) {
axios.post('https://identitytoolkit.googleapis.com/v1/accounts:signIn?key=[API_KEY]'), {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res)
commit('authUser', {
token: res.data.idToken,
userId: res.data.localId
})
localStorage.setItem('token', res.data.idToken)
localStorage.setItem('userId', res.data.localId)
router.push("/dashboard")
})
.catch(error => console.log(error))
}
}
设置身份验证守卫
现在我们已将令牌存储在应用程序中,我们将使用此令牌设置身份验证守卫。什么是身份验证守卫?它保护仪表盘免遭未经身份验证的用户在没有令牌的情况下访问它。
首先,我们将进入我们的路由文件并导入存储。导入存储是因为将确定用户的登录状态的令牌。
import store from './store.js'
然后,在我们的路由数组中,转到仪表盘路径并添加 beforeEnter
方法,它接受三个参数:to
、from
和 next
。在此方法中,我们只是说,如果令牌已存储(如果已通过身份验证,则会自动完成),那么 next
,这意味着它将继续执行指定的路由。否则,我们将未经身份验证的用户带回注册页面。
{
path: '/dashboard',
component: DashboardPage,
beforeEnter (to, from, next) {
if (store.state.idToken) {
next()
}
else {
next('/signin')
}
}
}
创建 UI 状态
在这一点上,我们仍然可以在导航中看到仪表盘,无论我们是否已登录,这不是我们想要的。我们必须在 getter 下添加另一个方法,称为 ifAuthenticated
,它检查状态中的令牌是否为 null
,然后相应地更新导航项目。
getters: {
user (state) {
return state.user
},
ifAuthenticated (state) {
return state.idToken !== null
}
}
接下来,打开标题组件并创建一个名为 auth
的方法,放在 computed
属性中。它将调度到我们在存储中创建的 ifAuthenticated
getter。ifAuthenticated
如果没有令牌,将返回 false
,这意味着 auth
也将为 null
,反之亦然。之后,我们添加一个 v-if
来检查 auth
是否为 null
,确定仪表盘选项是否显示在导航中。
<template>
<header id="header">
<div class="logo">
<router-link to="/">Vue Authenticate</router-link>
</div>
<nav>
<ul>
<li v-if='auth'>
<router-link to="/dashboard">Dashboard</router-link>
</li>
<li v-if='!auth'>
<router-link to="/signup">Register</router-link>
</li>
<li v-if='!auth'>
<router-link to="/signin">Log In</router-link>
</li>
</ul>
</nav>
</header>
</template>
<script>
export default {
computed: {
auth () {
return this.$store.getters.ifAuthenticated
}
},
}
</script>
注销
没有注销按钮的应用程序是什么?让我们创建一个名为 clearAuth
的新变异,它将令牌和 userId
都设置为 null
。
mutations: {
authUser (state, userData) {
state.idToken = userData.token
state.userId = userData.userId
},
clearAuth (state) {
state.idToken = null
state.userId = null
}
}
然后,在我们的 logout
操作中,我们提交到 clearAuth
,删除本地存储,并添加 router.replace('/')
以在注销后正确重定向用户。
回到标题组件。我们有一个 onLogout
方法,它将我们的 logout
操作调度到存储中。然后,我们向按钮添加一个 @click
,它调用 onLogout
方法,如下所示
<template>
<header id="header">
<div class="logo">
<router-link to="/">Vue Authenticate</router-link>
</div>
<nav>
<ul>
<li v-if='auth'>
<router-link to="/dashboard">Dashboard</router-link>
</li>
<li v-if='!auth'>
<router-link to="/signup">Register</router-link>
</li>
<li v-if='!auth'>
<router-link to="/signin">Log In</router-link>
</li>
<li v-if='auth'>
<ul @click="onLogout">Log Out</ul>
</li>
</ul>
</nav>
</header>
</template>
<script>
export default {
computed: {
auth () {
return this.$store.getters.ifAuthenticated
}
},
methods: {
onLogout() {
this.$store.dispatch('logout')
}
}
}
</script>
自动登录?当然!
我们离完成应用程序还有最后一步。我们可以注册、登录和注销,所有 UI 更改都已完成。但是,当我们刷新应用程序时,我们将丢失数据并注销,不得不重新开始,因为我们将令牌和 ID 存储在 Vuex 中,它是 JavaScript。这意味着应用程序中的所有内容在刷新时都会在浏览器中重新加载。
我们将从本地存储中检索令牌。通过这样做,我们可以让用户在刷新窗口时始终拥有令牌,甚至可以自动登录用户,只要令牌仍然有效。
创建一个名为 AutoLogin
的新操作方法,我们将在其中从本地存储获取令牌和 userId
,只有在用户拥有它们的情况下才会进行此操作。然后,我们将数据提交到变异中的 authUser
方法。
actions : {
AutoLogin ({commit}) {
const token = localStorage.getItem('token')
if (!token) {
return
}
const userId = localStorage.getItem('userId')
const token = localStorage.getItem('token')
commit('authUser', {
idToken: token,
userId: userId
})
}
}
然后,转到 App.vue
并创建一个 created
方法,当应用程序加载时,我们将从中调度存储中的 autoLogin
。
created () {
this.$store.dispatch('AutoLogin')
}
好了!有了它,我们已成功在应用程序中实现身份验证,现在可以使用 npm run build
进行部署。查看实时演示以了解它的实际运行情况。
示例网站仅用于演示目的。请在测试演示应用程序时不要共享真实数据,例如您的真实电子邮件和密码。
将令牌存储在本地存储中不被认为是最佳实践。本地存储中的数据在 XSS 攻击的情况下很脆弱。
有什么安全的选择吗?我在这里问的是无知的。
谁说的?唯一的选择是将它存储在 cookie 中,但这也有其漏洞。本地存储没有问题。Vue 的设计本就安全,可以抵御 XSS 攻击,除非你愚蠢地将用户输入作为 HTML 输出。
另外,下次请提供替代方案。[已屏蔽]
最重要的是完全防止 XSS 攻击,因为如果发生攻击,无论你使用 cookie 还是 localstorage,用户的会话都会被泄露。你可以在此处找到更多信息 https://forum.vuejs.org/t/vuejs-jwt/14976/19
此外,Vue 本身就能够抵御 XSS 攻击。
请不要将任何密码或访问令牌存储在 localstorage 中。任何在用户浏览器标签页中运行的 JavaScript 代码都可以读取它。
我使用 secure-ls 来加密 localStorage。
你不使用签名,https 很容易被绕过和伪造。使用 https 和像银行通信中使用的 sha512 密钥的签名是一个更好的做法,可以确保数据来源。
我们可以为这段漂亮的代码创建一个 github 项目吗?
也许我一开始没有看到 https://github.com/Atanda1/vue-auth
您好!
感谢这个教程,它非常有用。
为了让这段代码运行,我不得不将 AutoLogin 移动到路由器中。App.vue 的 create() 在路由器之后触发,导致页面总是重定向到登录页面。
不确定这是我的配置导致的,但似乎其他人也遇到了这个问题
https://stackoverflow.com/questions/45290003/router-beforeeach-guard-executed-before-state-loaded-in-vue-created
再次感谢。
有趣……我以前没有遇到过这种情况,因为它在 App.vue 中可以正常工作,但我一定会检查一下。
大家好,AuthLogin 操作中有一个小错误
困扰了我很久,只能通过与 David 的工作版本进行差异比较才能找到它 https://github.com/Atanda1/vue-auth/blob/master/src/store.js
不错的博客!不过,我有一个问题。在 store.js 中公开 API 密钥安全吗?这个密钥不会赋予管理员访问整个 Firebase 项目的权限吗?
这是一个错误,直到文章发布后我才发现,但我的 Firebase 数据库中实际上没有任何内容,而且空数据库有规则。此外,这里的所有内容都仅供演示之用。
嘿,我无法登录。我已经设置了 Firebase,但在登录时显示跨域错误。如何在 Firebase 中设置来源?
感谢分享,这是一个很棒的教程。几个问题:1) 如果用户提供错误的凭据,我该如何防止控制台中的 400 错误?2) 是否有一种方法可以使经过身份验证的用户被动态重定向到他们最初请求的路由,而不是教程中硬编码的仪表板?感谢。
1) 如果你不在
catch
块中console.log
错误,那么控制台中就不会出现错误。2) 这有可能,你只需要使用那个特定受保护路由的
beforeEach
即可。例如,对于仪表板路由,当没有令牌时,我们不会直接将用户重定向到登录页面,而是使用next('/signin')
我们改为使用
const signinpath = window.location.pathname;
next({ path: ‘/signin’, query: { redirect: signinpath } });
window.location.pathname
获取目标路由并将其作为名为redirect
的查询参数传递。然后,我们进入
signIn
操作。在用户登录后,我们不会像文章中那样硬编码仪表板router.push("/dashboard")
而是添加
router.push(this.$route.query.redirect)
这将获取之前传递的查询参数 (
redirect
) 并将用户发送到那里。在这里找到更多信息 https://stackoverflow.com/a/51034158
谢谢 David。在第 1 点上,我从 github 上获取了代码,在该目录中安装了 npm 并尝试了表单。当我在那里故意提交错误的登录信息时,我在控制台中遇到了同样的错误
“无法加载资源:服务器响应状态为 400 ()”
在以下代码段中,似乎:o.apply(e,arguments)}))),”function”==typeof fetch&&(fetch=e(fetch,(r=fetch,function(t,e)
这说得通,因为登录信息不正确,但想知道是否有一种方法可以让它在不抛出错误的情况下进行响应。也许这仅仅是因为我在本地开发环境中运行它?
请原谅这些可能愚蠢的问题。我对此很陌生,感谢您的帮助 ;-)