这里的用户鉴权是前端的版本,正好这个视频还有关于前端用户鉴权的教程,就来先学习一下。由于后端可以使用Firebase,所以很方便。

到了Java的时候,再详细来试验Shiro框架和JWT鉴权的部分。

  1. 前后端分离用户鉴权的理论
  2. 配置firebase
  3. 使用Firebase API和鉴权
  4. 鉴权的其他应用

前后端分离用户鉴权的理论

在原始的后端渲染网页的Web应用中,前后端通过Session来确认用户状态。而纯粹的前后端分离只通过JSON数据进行交流,所以一般的方式是登录的时候将用户名和密码发送之后,服务端返回一个token,之后每次都需要将token发送到后端。token一定时间之后还会变化。根据每次发送token的响应,决定用户是否具有相关权利。

一般用户数据都是一个全局的状态,所以存放在Vuex中。

authenticate in SPA

配置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不外乎注册用户和鉴权两大功能:

  1. 注册用户
  2. 获取TOKEN
  3. 使用Vuex存储TOKEN
  4. 验证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的属性:

  1. email,字符串,用户邮件地址
  2. password,字符串,密码
  3. 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"

其中的主要字段是:

  1. expiresIn表示到期的秒数
  2. idToken为当前用户的鉴权TOKEN
  3. refreshToken用来刷新用户的鉴权TOKEN
  4. 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.vuesignup.vue去掉发送异步请求的代码,换成启动storeaction的代码:

//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即可。

鉴权的其他应用

在上一节读取数据库,实际上是鉴定某项操作有没有权限。常用的还有如下操作:

  1. 保护访问路径:通过设置导航守卫的beforeEach来实现,如果鉴权成功就进入,不成功就跳转登录界面。
  2. 自动注销。自动注销的功能要和后端搭配使用。在data中返回的idToken会过期。由于一刷新页面,Vuex中的数据会消失,所以常和本地存储结合起来。
  3. 保持登录状态,也必须和本地存储结合起来,可能还需要针对具体客户端附加其他信息。

注销和保护访问路径都不难,关键要看一下保持登录状态,也就是使用本地存储的方法。

业务逻辑上来说,就是每次注册和登录成功的时候,向本地存储内放入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玩玩了。