这里的用户鉴权是前端的版本,正好这个视频还有关于前端用户鉴权的教程,就来先学习一下。由于后端可以使用Firebase,所以很方便。
到了Java的时候,再详细来试验Shiro框架和JWT鉴权的部分。
- 前后端分离用户鉴权的理论
- 配置firebase
- 使用Firebase API和鉴权
- 鉴权的其他应用
前后端分离用户鉴权的理论
在原始的后端渲染网页的Web应用中,前后端通过Session来确认用户状态。而纯粹的前后端分离只通过JSON数据进行交流,所以一般的方式是登录的时候将用户名和密码发送之后,服务端返回一个token,之后每次都需要将token发送到后端。token一定时间之后还会变化。根据每次发送token的响应,决定用户是否具有相关权利。
一般用户数据都是一个全局的状态,所以存放在Vuex中。
配置firebase
Firebase可以很容易的充当后端来做实验。
在左侧的开发-Authentication
中选择设置登录方法
,然后会列出一堆验证方式,选择电子邮件地址/密码
认证,然后启用。到Database
中的规则/Rules
中,修改为如下内容:
{
"rules": {
".read": "auth != null",
".write": true
}
}
这么修改意味着如果想要访问数据库,必须登录才行,否则无法访问。启动刚才的AXIOS项目,访问/dashboard
,浏览器控制台中提示:
Failed to load resource: the server responded with a status of 401 (Unauthorized)
说明我们设置的验证功能生效了。
在鉴权之前,可以到Firebase DataBase REST API文档看一下要使用API。
使用Firebase API和鉴权
使用Firebase API不外乎注册用户和鉴权两大功能:
- 注册用户
- 获取TOKEN
- 使用Vuex存储TOKEN
- 验证TOKEN
注册用户
文档的用户鉴权文档里列出了如何注册新用户:
Sign up with email / password
You can create a new email and password user by issuing an HTTP POST request to the Auth signupNewUser endpoint.
Method: POST
Content-Type: application/json
Endpoint
https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key=[API_KEY]
这个URL前边的部分,也就是https://www.googleapis.com/identitytoolkit/v3/relyingparty
是固定的,后边的/signupNewUser?key=[API_KEY]
是要获取我们自己创建的API KEY才能够使用。
所以在我们的项目里,设置如下:
//main.js
axios.defaults.baseURL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/';
//signup.vue
axios.post('/signupNewUser?key=[API_KEY]', formData)
现在还没有API_KEY
,按照文档提示到项目设置中,在常规
选项卡中能看到网络API密钥,然后拼合到代码里。
继续看文档,下边列出了请求体的载荷,即POST到指定地址的JSON的属性:
email
,字符串,用户邮件地址
password
,字符串,密码
returnSecureToken
,布尔值,是否返回用户的ID和Token,必须设置为true。
继续修改axios要发送的数据:
axios.post('/signupNewUser?key=AIzaSyAtwQ-RL7GKu0T1h4kOy32JRXmbmQPqiHM', {
email: formData.email,
password: formData.password,
returnSecureToken: true
})
JSON对象中设置好了文档要求的三个属性。然后回到Sign Up页面,这次填入用户名和密码来试试新增用户。
在Sign up页面中填写数据,然后发送,看控制台中出现了响应,尤其注意其中的data部分:
data:
email: "xxxx@xxxx.com"
expiresIn: "3600"
idToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjY2NDNkZDM5ZDM4ZGI4NWU1NjAxN2E2OGE3NWMyZjM4YmUxMGM1MzkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbXlheGlvcy02NjY2NiIsImF1ZCI6Im15YXhpb3MtNjY2NjYiLCJhdXRoX3RpbWUiOjE1NTg3NTMyMDYsInVzZXJfaWQiOiIwS2x0NWRiOE5RWHpJYkR2YlVtZzVyd2FqcHoyIiwic3ViIjoiMEtsdDVkYjhOUVh6SWJEdmJVbWc1cndhanB6MiIsImlhdCI6MTU1ODc1MzIwNiwiZXhwIjoxNTU4NzU2ODA2LCJlbWFpbCI6ImxlZTA3MDlAdmlwLnNpbmEuY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbImxlZTA3MDlAdmlwLnNpbmEuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.pwejTv7Aa1CwM95f527MF26p0BOb3cKE4f2fP06p349Y48eeMwxwz3hD0697iT3fzjha3YWyzUzbQt13FDxNQe0kWqTv890EnCBMwWBI_IWN-B6LoQ37BwtJhkx2StJEYnaa6mDwe-4tenyEJQ-FAFK-Tmb2wk4dHBKppgW6pMwwLfnAhtwXaF_2o8b2SxC-O5Kug__NmuPSrQLbgx2n1lFdPAdp42ojo_zWPNshnzerUwODeeYpQlyAf1wcjTieRifqF4wgbTss1SNk8CO8-k0o8gOQZzB-HyopmnW2m--XQtg9LmrQYxBv5MHqirwaCq4WRZ5EJiocO2SZevEUQA"
kind: "identitytoolkit#SignupNewUserResponse"
localId: "0Klt5db8NQXzIbDvbUmg5rwajpz2"
refreshToken: "AEu4IL2z8XuB9f4_fs_MQdoTxwMFvmzgEBwyaG5MLpUOnl9Jq7iXSX4g9KG7Q5EEN_ryEF8OYRB7AabbrdGi1NtrKS5wIUu7oyxfMBuUb28gVVGZZ88celYUnHyZKxece3mNvS5u2ZMmuJCTE_Piburs3qU3Jr79rZJIelf0dOf1RA4TkfEDgp5gNpuc23cRFpTaNOkQSvScxo_Dz_AUELnbzL8jiOimKw"
其中的主要字段是:
expiresIn
表示到期的秒数
idToken
为当前用户的鉴权TOKEN
refreshToken
用来刷新用户的鉴权TOKEN
localId
,存储在Firebase数据库的UID,到控制面板即可看到。
这个时候对于我们来说,就获得了刚注册的用户的身份鉴权的数据。到Firebase的控制面板,也能够发现成功注册的用户的信息。
获取TOKEN
这是最重要的用户鉴权部分的第一步,也就是获取TOKEN,即如何使用获取到的用户ID。
首先来看用户登录,也就是获取到TOKEN,先找到文档中的API解说:
Sign in with email / password
You can sign in a user with an email and password by issuing an HTTP POST request to the Auth verifyPassword endpoint.
Method: POST
Content-Type: application/json
Endpoint
https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=[API_KEY]
发现需要将用户名和密码POST到另外一个地址,载荷的属性和注册的时候是一样的。
现在到signin.vue
中来编写代码:
import axios from 'axios';
methods: {
onSubmit () {
const formData = {
email: this.email,
password: this.password,
};
axios.post('/verifyPassword?key=AIzaSyAtwQ-RL7GKu0T1h4kOy32JRXmbmQPqiHM', {
email: this.email,
password: this.password,
returnSecureToken: true
}).then(response => console.log(response));
}
}
然后到登录页面,输入已经存在数据库的邮件和密码,成功获得200响应,查看控制台的响应对象中的data
属性:
displayName: ""
email: "xxxx@xxxx.com"
expiresIn: "3600"
idToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6IjY2NDNkZDM5ZDM4ZGI4NWU1NjAxN2E2OGE3NWMyZjM4YmUxMGM1MzkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbXlheGlvcy02NjY2NiIsImF1ZCI6Im15YXhpb3MtNjY2NjYiLCJhdXRoX3RpbWUiOjE1NTg3NjU3MTEsInVzZXJfaWQiOiIwS2x0NWRiOE5RWHpJYkR2YlVtZzVyd2FqcHoyIiwic3ViIjoiMEtsdDVkYjhOUVh6SWJEdmJVbWc1cndhanB6MiIsImlhdCI6MTU1ODc2NTcxMSwiZXhwIjoxNTU4NzY5MzExLCJlbWFpbCI6ImxlZTA3MDlAdmlwLnNpbmEuY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbImxlZTA3MDlAdmlwLnNpbmEuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.LkbsMqGnuT9vGlhSPT8rR17B6XFRwyAtHoThvxBqBOtQRJQskvh8qTSueBdIhZ5Vp5a0TTG6p-CE9WKl__tcgylldz2M8wBzRiBnUUV9irY3vFzS53_MNdmPdMJIzBXIDozCxgjiQPfkzU-pxpsmQ2iwHeNFlsKWT44z_yaTYjrbZR5LwhYSirQsQJJ5TFyJDOobqJ9Dsp-xJ-f__1Cz5barn0rUt0O_tIlYJmUlGqMv6a3cP3cXT6IGN3WTNUBDYJhBOtCWpKZhH-damQt5pYUjutNmZUDxJhYxoplYTGnHwaNb4MmtIyIMXooBiHsR4gmq1pIIUMsxzsWk9P3O3g"
kind: "identitytoolkit#VerifyPasswordResponse"
localId: "0Klt5db8NQXzIbDvbUmg5rwajpz2"
refreshToken: "AEu4IL0b-fsqF8n1HPgOR9pjPOaIwryeO65C08BVB2uAl-ti_FBTVURRRps8gWaPYkzVUAJ9DZQU2RUPn6eWFYp-k1FyNGL1_4idFA-PibxB5G1StN0gQMkmeXeePgEq07WXhJ2MaVgDsezsiKCr-HBhL8WO1wA-uBuMY-5_JA8NAYD5PxGSEgfnxaELXCtxinwnaEJm_hcWQDcXP6N0yiGogfSAXqyRdw"
registered: true
和注册时候返回的内容基本一样,多了一个regiestered
表示是已经注册的用户。而新注册则没有这个属性。
由于是无状态,我们只是获取了一个TOKEN而已,如果此时访问/dashboard
页面,由于我们还没有使用TOKEN去鉴权,但已经在Firebase了启用了访问数据库需要鉴权,控制台里依然会显示401错误。
注意由于修改了默认前缀,最好把/dashboard
里访问路径改成绝对路径:https://myaxios-66666.firebaseio.com/user-admin.json
。
使用Vuex存储TOKEN
现在要做的一步,就是想办法使用获取到的TOKEN来通过服务器的验证,让/dashboard
页面读出信息来。
首先要解决的问题是,我们获取到的TOKEN存放在哪里。这个TOKEN因为需要反复使用,而用户一般登录是一个全局状态,所以我们一般将其放在Vuex中。
为了存放上一步获取到的TOKEN等信息,新创建一个store:
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
idToken: null,
userId: null
},
mutations: {
authUser: function (state, userData) {
console.log("AuthUser执行了");
state.idToken = userData.token;
state.userId = userData.userId;
}
},
actions: {
signup({commit}, authData) {
axios.post('/signupNewUser?key=AIzaSyAtwQ-RL7GKu0T1h4kOy32JRXmbmQPqiHM', {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res);
commit('authUser', {
token: res.data.idToken,
userId: res.data.localId
})
})
.catch(err => console.log(err));
},
login({commit}, authData) {
axios.post('/verifyPassword?key=AIzaSyAtwQ-RL7GKu0T1h4kOy32JRXmbmQPqiHM', {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res);
commit('authUser', {
token: res.data.idToken,
userId: res.data.localId
})
})
.catch(err => console.log(err));
}
},
getters: {}
})
然后在signin.vue
和signup.vue
去掉发送异步请求的代码,换成启动store
的action
的代码:
//signup.vue
this.$store.dispatch('signup', {
email: formData.email,
password: formData.password,
returnSecureToken: true
}).then(() => console.log("action-signup执行完成"));
//signin.vue
this.$store.dispatch("login", {
email: formData.email,
password: formData.password,
returnSecureToken: true
}).then(() => console.log("action-login执行完毕"));
虽然代码多但是逻辑很简单。登录或者注册后用表单数据提交给actions执行--actions发送请求至后端进行验证--验证成功就commit到store中。
在其中除了设置好保存用户TOKEN等相关信息之外,我们还直接给action设置上异步去获取TOKEN的方法,包括新注册和登录两个方法。只要新注册或者登录,都会更新用户状态。
实际实验一下,发现注册和登录之后,store中已经有了对应的内容。那么现在就需要使用这个TOKEN来通过服务器的验证,查询用户信息了。
验证TOKEN
注意我们保存在Vuex中的是用户的UID和TOKEN两个信息,很显然,验证身份就需要这两个信息。为了方便,把获取信息的代码也放到store中来:
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
idToken: null,
userId: null,
userdata: null
},
mutations: {
authUser: function (state, userData) {
console.log("AuthUser执行了");
state.idToken = userData.token;
state.userId = userData.userId;
},
storeUser: function (state, userData) {
state.userdata = userData.userdata;
}
},
actions: {
signup({commit}, authData) {
axios.post('/signupNewUser?key=AIzaSyAtwQ-RL7GKu0T1h4kOy32JRXmbmQPqiHM', {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res);
commit('authUser', {
token: res.data.idToken,
userId: res.data.localId
})
})
.catch(err => console.log(err));
},
login({commit}, authData) {
axios.post('/verifyPassword?key=AIzaSyAtwQ-RL7GKu0T1h4kOy32JRXmbmQPqiHM', {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res);
commit('authUser', {
token: res.data.idToken,
userId: res.data.localId
})
})
.catch(err => console.log(err));
},
fetchData(context) {
if (context.state.idToken && context.state.userId) {
axios.get('https://myaxios-66666.firebaseio.com/user-admin.json' + '?auth=' + context.state.idToken)
.then(res => {
console.log("fetchUser执行了");
context.commit('storeUser', {
userdata: res.data
});
})
.catch(err => console.log(err));
}
}
},
getters: {}
})
此时dashboard.vue
的代码就是启动action:
<template>
<div id="dashboard">
<h1>That's the dashboard!</h1>
<p v-if="!userdata">You should only get here if you're authenticated!</p>
<p v-if="userdata" v-for="user in userdata">{{user}}</p>
</div>
</template>
<script>
export default {
computed:{
userdata: function () {
return this.$store.state.userdata;
}
},
created() {
this.$store.dispatch('fetchData').then(()=>console.log("getUser执行完毕"));
}
}
</script>
<style scoped>
h1, p {
text-align: center;
}
p {
color: red;
}
</style>
新加的红色部分的逻辑是:进入dashboard
--触发执行action-fetchData
--异步发送带有验证TOKEN的请求到后端--把后端返回的数据commit到Vuex中--Dashboard渲染出结果。
其中的关键,就是:
axios.get('https://myaxios-66666.firebaseio.com/user-admin.json' + '?auth=' + context.state.idToken)
中的红色部分,即使用?auth=idToken
来附加在请求后边通过验证。
运行程序,先去注册或者登录,然后转到Dashboard界面,就会发现列出来所有要查找的数据,说明鉴权成功了。
在Firebase里此时我们只设置了读取数据库需要鉴权,如果将写入数据库也设置为需要鉴权,则拼接POST请求的时候,也加上?auth=idToken
即可。
鉴权的其他应用
在上一节读取数据库,实际上是鉴定某项操作有没有权限。常用的还有如下操作:
- 保护访问路径:通过设置导航守卫的beforeEach来实现,如果鉴权成功就进入,不成功就跳转登录界面。
- 自动注销。自动注销的功能要和后端搭配使用。在
data
中返回的idToken
会过期。由于一刷新页面,Vuex中的数据会消失,所以常和本地存储结合起来。
- 保持登录状态,也必须和本地存储结合起来,可能还需要针对具体客户端附加其他信息。
注销和保护访问路径都不难,关键要看一下保持登录状态,也就是使用本地存储的方法。
业务逻辑上来说,就是每次注册和登录成功的时候,向本地存储内放入TOKEN。而刷新页面的时候,整个Vue实例包括Vuex都会重新创建,创建的时候尝试从本地存储中读入数据并使用即可。
保存TOKEN
先来在登录逻辑中编写一下向本地存储中存入当前TOKEN的代码,修改store中的login方法:
login({commit}, authData) {
axios.post('/verifyPassword?key=AIzaSyAtwQ-RL7GKu0T1h4kOy32JRXmbmQPqiHM', {
email: authData.email,
password: authData.password,
returnSecureToken: true
})
.then(res => {
console.log(res);
commit('authUser', {
token: res.data.idToken,
userId: res.data.localId
});
const now = new Date().getTime();
const expirationDate = now + res.data.expiresIn * 1000;
localStorage.setItem('token', res.data.idToken);
localStorage.setItem('expiresDate', expirationDate);
})
.catch(err => console.log(err));
},
在登录成功并且获取到TOKEN之后,计算出登录的过期时间,然后把过期时间和TOKEN同时保存到本地存储中。之后在页面中登录,然后打开Chrome开发工具-Application-Local Storage中可以看到保存的键值对。
读取登录状态
由于我们是单页面应用,页面刷新的时候意味着整个应用都被重置到新的状态,只有本地存储不会变化。
所以我们要在一个合适的时点将本地存储的内容读回来。由于一般的业务逻辑,我们都是创建好了Vuex对象和路由对象并且注入到根实例之后,最后挂载根Vue实例。
所以业务逻辑可以写在根实例创建或者挂载的生命周期函数中,只要页面刷新就尝试去读取一下。
所以我们先给Vuex再添加一个action函数,让其装载对应的本地存储内容。然后在根实例的创建过程中,执行这个action:
//store.js 中的 actions部分
tryAutoLogin(context) {
const token = localStorage.getItem('token');
const expireDate = localStorage.getItem('expiresDate');
const now = (new Date()).getTime();
if ((now <= expireDate) && token) {
context.commit("authUser", {
token: token,
userId: null
});
}
}
在这个函数中,尝试从本地存储读出TOKEN和过期日期,然后获取当前时间。仅当当前时间小于过期日期,并且TOKEN不为空的情况下,向store中装载TOKEN。
之后在main.js
中,在Vue根实例创建的过程中dispatch
一下:
new Vue({
el: '#app',
router,
store,
render: h => h(App),
created: function () {
store.dispatch("tryAutoLogin").then(() => console.log("成功装载信息"));
}
这样就实现了保存状态,对于注册成功等操作也是一样的。
这是最基础的应用,还可以在Vue实例创建的时候去检测本地存储是否过期,过期就删除本地存储。本地存储的东西也需要简单了解一下。
剩下就是学习编写支持JWT验证的后端,以及好好的写一个SPA玩玩了。