Vue 项目中用户登录及 token 验证的思路

9,349 阅读4分钟

前言

在讲解 token 验证之前,我们先来聊聊 vue-router 的导航守卫与 axios 拦截器这两个知识点。

vue-router 导航守卫

所谓“导航”,即路由正在发生变化。可在路由跳转时完成一些操作,而 router.beforeEach() 全局前置守卫可以在路由跳转前对现在状态进行校验,例如验证用户的登录状态,若未登录则可以有效进行拦截。

const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  // ...
})

axios 拦截器

axios 拦截器又分为请求拦截器响应拦截器,可在请求或响应被 thencatch 处理前拦截它们,即在前端页面向后端发送请求时触发进行拦截。

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
	// 在发送请求之前做些什么
    return config;
}, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response;
}, function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error);
});

如果你稍后需要移除拦截器,可以这样:

const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);

区别

导航守卫更像是路由级拦截,而 axios 拦截器则是接口级拦截,这是它们本质上的区别。

关于它们更详细的区分在后文会提及,这里先不讨论,因为知识点没有带入到实例场景中进行应用容易造成理解不深或理解不了的问题,先回归正文。

Vue 项目实现 token 验证

401 Unauthorized:该状态码表示用户所发送的请求没有访问权限,需要进行身份认证。返回含有 401 的响应必须包含一个适用于被请求资源的 WWW-Authenticate 首部用以质询用户信息。

前后端分离开发方式固然好,但也带来了一些棘手的问题,如跨域处理token 验证..

接下来展开说说 token 验证。

在 Vue 项目中实现 token 验证的大致思路如下:

  1. 第一次登录时,前端调用后端的登录接口,发送用户名和密码
  2. 后端收到请求后,验证用户名和密码的合法性,成功则给前端返回一个 token
  3. 前端拿到 token 后,将其存储到 localStoragevuex 中,并跳转路由页面
  4. 此后前端每次跳转路由时,就判断 localStorage 中有无 token,没有则跳转到登录页面,有则跳转到对应的路由页面
  5. 且前端每次调用后端接口时,都要在请求头中携带 token
  6. 后端判断请求头中有无 token
    • header 中存在 token:验证 token 的合法性,成功则返回前端请求的数据;失败(如 token 过期)就返回 401 状态码(401 Unauthorized
    • header 中不存在 token:同样返回 401 状态码
  7. 如果前端接收到 401 状态码,则清除 token 信息并跳转到登陆页面

Login.vue

<template>
  <div>
    <input type="text" v-model="loginForm.username" placeholder="用户名"/>
    <input type="text" v-model="loginForm.password" placeholder="密码"/>
    <button @click="login">登录</button>
  </div>
</template>
 
<script>
import { mapMutations } from 'vuex';
export default {
  data () {
    return {
      loginForm: {
        username: '',
        password: ''
      },
      userToken: ''
    };
  },
 
  methods: {
    ...mapMutations(['changeLogin']),
    login () {
      let _this = this;
      if (this.loginForm.username === '' || this.loginForm.password === '') {
        alert('账号或密码不能为空');
      } else {
        this.axios({
          method: 'post',
          url: '/user/login',
          data: _this.loginForm
        }).then(res => {
          console.log(res.data);
          _this.userToken = 'Bearer ' + res.data.data.body.token;
          // 将用户token保存到vuex中
          _this.changeLogin({ Authorization: _this.userToken });
          _this.$router.push('/home');
          alert('登录成功');
        }).catch(error => {
          alert('账号或密码错误');
          console.log(error);
        });
      }
    }
  }
};
</script>

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
 
const store = new Vuex.Store({
  state: {
    // 存储token
    Authorization: localStorage.getItem('Authorization') ? localStorage.getItem('Authorization') : ''
  },
  mutations: {
    // 修改token,并将token存入localStorage
    changeLogin (state, user) {
      state.Authorization = user.Authorization;
      localStorage.setItem('Authorization', user.Authorization);
    }
  }
});
 
export default store

router/index.js: 导航守卫

import Vue from 'vue';
import Router from 'vue-router';
import login from '@/components/login';
import home from '@/components/home';
 
Vue.use(Router);
 
const router = new Router({
  routes: [
    {
      path: '/',
      redirect: '/login'
    },
    {
      path: '/login',
      name: 'login',
      component: login
    },
    {
      path: '/home',
      name: 'home',
      component: home
    }
  ]
});

// 导航守卫:使用 router.beforeEach 注册一个全局前置守卫,判断用户是否登陆
router.beforeEach((to, from, next) => {
  if (to.path === '/login') {
    next();
  } else {
    let token = localStorage.getItem('Authorization');
 
    if (token === null || token === '') {
      next('/login');
    } else {
      next();
    }
  }
});

export default router

main.js: 请求拦截器

// 请求拦截器, 每次请求都会在请求头中携带token
axios.interceptors.request.use((config) => {
  if(localStorage.getItem('Authorization')) {
    config.headers.Authorization = localStorage.getItem('Authorization')
  }
  return config;
}, (error) => {
  return Promise.reject(error);
})

401 Unauthorized 处理

如果前端拿到后端返回的 401 状态码,则清除 token 信息并跳转到登陆页面。

localStorage.removeItem('Authorization');
this.$router.push('/login');

导航守卫请求拦截器

导航守卫仅仅简单判断是否有 token 值存在(不管该 token 是否有效),如果不存在/失效就进行重定向。

请求拦截器是向后端发送请求并校验,如果 token 合法就访问成功,否则访问失败并进行重定向。

⭐总之,在进行页面权限判断时通常都是让 vue-router 导航守卫axios 拦截器搭配使用!

FAQ

Q:为什么不在导航守卫中顺便也发送请求来校验 token 是否过期,这样 token 过期后在用户进行页面跳转时不就可以及时提醒用户重新登录了吗?
A:token 本身就是用于进行接口鉴权,token 过期以后,就无法再请求数据了。在如今单页面应用中,访问有的页面时不需要和后端进行交互,即没有接口请求,那跟 token 接口鉴权无关,就无需进行校验。而只要经过了拦截器必然就是一次接口的请求,所以是给拦截器进行验证是否过期。导航守卫做验证当然可以,不过会因为给没有交互的页面做验证,而造成无效的资源浪费


🙎‍♂️博客引用: