Vue项目 使用拦截器和JWT验证 完整案例

4,800 阅读3分钟

文章已同步至【个人博客】,欢迎访问【我的主页】😃
文章地址:blog.fanjunyang.zone/archives/vu…

几乎在所有的项目中都离不开拦截器登录验证,这是必需的。如果你学会了这个demo,那么几乎所有网站的登录验证,加载动画就都会了,所以背也要背会

所以本章以一个demo为例,来帮助大家理解拦截器登录验证控制

文章后面有源码,可以下载下来运行一下

先来看看效果:

功能:

  • 当你访问首页的时候,会有一个加载动画,就是拦截器的功劳,并且首页会有一个当前登录的用户名,默认是wangcai,等你登录成功后,会替换成你自己登录的用户名

  • 当你没有登录的时候,可以访问首页和登录页,但是访问不了个人中心(Profile),当你访问个人中心,会给你自动跳转到登录页

  • 当你在登录页进行登录,如果用户名输入错误的话,会弹出错误信息

  • 当你输入正确的时候(我设置了Fan为正确的用户名),点击登录,登录成功后,会自动给你跳转到首页

  • 并且登录成功后,如果你再点击想进入登录页,是不行的,他会自动给你跳转到首页

  • 登录成功后,就可以访问 个人中心页面

  • 如果你超过 20秒 不对页面进行操作(我设置的是20秒,可以自行设置),那么token会自动失效,那么你就访问不了个人中心,你需要再次登录

  • 如果你在 20秒 之内,操作页面的话,那么token的值是不会失效的,所以是不需要再次登录的。也就是说,在 20秒 之内,你每次进行路由跳转的时候,token的值和时间就会自动重置,防止失效让你再次登录(总不能让你看着看着突然让你登录)

下面就让我们开始吧!!! (有关代码的解释说明已在代码中注释

案例

使用拦截器并封装axios

新建一个Vue项目(vue create demo)

删去不必要的文件和代码,经典化代码

安装需要的依赖:

package.json文件部分代码:

"dependencies": {
	"axios": "^0.19.0",
	"body-parser": "^1.19.0",
	"core-js": "^2.6.5",
	"express": "^4.17.1",
	"iview": "^4.0.0-rc.4",
	"jsonwebtoken": "^8.5.1",
	"vue": "^2.6.10",
	"vue-router": "^3.0.3",
	"vuex": "^3.0.1"
},

server.js文件中配置跨域,并书写测试接口:

let express = require('express')
let bodyParser = require('body-parser')

let app = express()

// 配置跨域
app.use((req, res, next) => {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS,POST,PUT"),
    res.header("Access-Control-Allow-Headers", "Origin,X-Requested-With,Content-Type,Accept,Authorization")
    if (req.method.toLowerCase() === "options") {
        return res.end();
    }
    next();
})

// 配置bodyparser
app.use(bodyParser.json())

app.get("/user", (req, res) => {
    //在请求数据时,要加一个动画,为了测试,所以让它时间长点,加了一个定时器
    setTimeout(() => {
        res.json({
            name: "wangcai"
        })
    }, 500)
})

app.listen(3000)

router.js中配置路由:

routes: [
	{
	  path: '/',
	  name: 'home',
	  component: Home
	},
	{
	  path: '/login',
	  name: 'login',
	  component: () => import('./views/Login.vue')
	},
	{
	  path: '/profile',
	  name: 'profile',
	  component: () => import('./views/Profile.vue')
	}
]

因为项目中需要用到样式什么的,这里我引入了iViewmain.js代码:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

//引入iView
import iView from 'iview'
import 'iview/dist/styles/iview.css';
Vue.use(iView)

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

因为项目中要用到加载数据的动画,所以需要在store.js中的state中配置:

state: {
	//定义动画是否显示
	isShowLoading:false,
	username:'wangcai'
	},
	mutations: {
	//使动画显示
	showLoading(state){
	  state.isShowLoading = true;
	},
	//使动画隐藏
	hideLoading(state){
	  state.isShowLoading = false;
	}
},

App.vue中配置跳转:

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/login">Login</router-link> |
      <router-link to="/profile">Profile</router-link>
    </div>
    <router-view/>
  </div>
</template>

Home.vue代码:

<template>
  <div class="home">
    <h1>首页面</h1>
  </div>
</template>

Login.vue代码:

<template>
    <div>
        <i-input placeholder="请输入用户名..." style="width: 300px"></i-input>
        <i-button type="primary">登录</i-button>
    </div>
</template>

Profile.vue代码:

<template>
<div>
    <h1>个人中心</h1>
</div>
</template>

然后在libs文件夹下面新建一个ajaxRequest.js文件,用来封装我们自己的 axios 和 加载动画 等

import axios from 'axios'
import store from '../store'

//当第一次请求时,显示loading  
class AjaxRequest {
    //当new的时候,调用这个方法
    constructor() {
        //请求的基础路径
        this.baseURL = process.env.NODE_ENV == "production" ? "/" : "http://localhost:3000"
        this.timeout = 3000 //超时时间
        this.queue = {} //存放每一次的请求
    }
    //定义一个方法,把options展开
    merge(options) {
        return {
            ...options,
            baseURL: this.baseURL,
            timeout: this.timeout
        }
    }
    //封装一个拦截方法
    setInterceptor(instance, url) {
        //请求拦截,每次请求时,都要加上一个loading效果
        instance.interceptors.request.use((config) => {
            //每次请求时,都给他加一个Authorization头,在JWT验证时要用
            config.headers.Authorization = 'xxx'
            //第一次请求时,显示loading动画
            if (Object.keys(this.queue).length === 0) {
                store.commit('showLoading')
            }
            this.queue[url] = url;
            return config
        })
        //响应拦截  
        instance.interceptors.response.use((res) => {
            //删除queue里面的链接,如果同一个按钮,你一秒之内点击无数次,但是他只处理第一次操作
            delete this.queue[url]
            //隐藏loading动画
            if (Object.keys(this.queue).length === 0) {
                store.commit('hideLoading')
            }
            //返回的结果
            return res.data
        })
    }
    request(options) {
        let instance = axios.create()   //创建一个axios实例
        this.setInterceptor(instance, options.url) //设置拦截
        let config = this.merge(options)
        return instance(config)     //axios执行后,返回promise
    }
}

export default new AjaxRequest;

然后在api文件夹下新建一个user.js文件用来放用户相关的调用接口的方法(当你想要调用接口的时候,直接调用里面的方法就好):

import axios from '../libs/ajaxRequset'

// 用户相关的接口
export const getUser = ()=>{
    return axios.request({
        url:'/user',
        method:'get'
    })
}

修改Home.vue中的代码:

<template>
  <div class="home">
    <h1>首页面</h1>
    <p>当前登录的用户名是{{$store.state.username}}</p>
  </div>
</template>

<script>
//如果用export导出的话,要用这种形式,相当于解构赋值
import {getUser} from '../api/user'
export default {
  name:'home',
  async mounted(){
    let r = await getUser()
    console.log(r);
  }
}
</script>

修改App.vue中的代码(加动画效果):

<template>
  <div id="app">
    <Spin size="large" fix v-if="$store.state.isShowLoading">
      加载中...
    </Spin>
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/login">Login</router-link> |
      <router-link to="/profile">Profile</router-link>
    </div>
    <router-view/>
  </div>
</template>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
#nav {
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}
</style>

运行效果:

使用JWT

server.js中新增代码,使用JWT,并写好登录和验证的接口:

let jwt = require('jsonwebtoken')

let secret = "xwc"

//登录的接口
app.post('/login',(req,res)=>{
    let {username} = req.body;
    if(username === 'Fan'){
        //登录成功后返回一个token
        res.json({
            code:0,
            username:'Fan',
            token:jwt.sign({username:'Fan'},secret,{
                expiresIn:20    //表示token20秒过期
            })
        })
    }else{
        //登录失败
        res.json({
            code:1,
            data:'登录失败了'
        })
    }
})

//验证token的接口
app.get('/validate',(req,res)=>{
    let token = req.headers.authorization;  //我们会把token放到我们自己设置的http的头authorization中,在这里可以直接拿到
    jwt.verify(token,secret,(err,decode)=>{     //验证token
        if(err){
            return res.json({
                code:1,
                data:'token失效了'
            })
        }else{
            // token合法  在这里,需要把token的时效延长,
            //总不能我们看着看着突然让我们重新登录,token过期的意思是,你在这之间不进行任何操作才会过期
            res.json({
                code:0,
                username:decode.username,
                token:jwt.sign({username:'Fan'},secret,{    //合法时,我们需要重新生成一个token,我们每次切换路由,都要重新生成一个token
                    expiresIn:20
                })
            })
        }
    })
})

接着在api文件夹下的user.js文件中添加登录和验证的方法:

import axios from '../libs/ajaxRequest'

// 用户相关的接口

// 调获取用户信息的接口  向外暴露一个getUser方法  这个方法中调了接口
// 在组件中,就可以使用getUser,就相当于调用接口
export const getUser = ()=>{
    return axios.request({
        url:'/user',
        method:'get'
    })
}

// 再向外暴露一个登录的方法,方法内部也是调接口
// 在登录组件中就可以调用Login方法,需要给方法传递一个用户名
export const login = (username)=>{
    return axios.request({
        url:'/login',
        method:'post',
        data:{
            username
        }
    })
}

//验证token方法  
export const validate = ()=>{
    return axios.request({
        url:'/validate',
        method:'get'
    })
}

接着我们在lib文件夹下新建一个local.js文件,用来设置或者获取localStorage里的token

//把获得到的token存到localStorage里
export const setLocal = (key,value)=>{
    if(typeof value == 'object'){   //如果传过来的是对象,则转换成字符串
        value = JSON.stringify(value)
    }
    localStorage.setItem(key,value)     //存到localStorage里
}

//获取localStorage里的token
export const getLocal = (key)=>{
    return localStorage.getItem(key)
}

然后修改store.js中的代码:

import Vue from 'vue'
import Vuex from 'vuex'
import {login,validate} from './api/user'   //必须用这种方式引入
import {setLocal} from './libs/local'   //引入lib文件夹下的local.js文件中的setLocal方法(往localStorage里存放token)

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    //定义动画是否显示
    isShowLoading:false,
    username:'wangcai'
  },
  mutations: {
    //使动画显示
    showLoading(state){
      state.isShowLoading = true;
    },
    //使动画隐藏
    hideLoading(state){
      state.isShowLoading = false;
    },
    //定义修改用户名的方法
    setUser(state,username){
      state.username = username
    }
  },
  // actions存放接口的调用  dispatch actions里面放方法
  actions: {
    //这里面所有的方法都是异步的

    //登录方法
    async toLogin({commit},username){
      let r = await login(username) //调用user.js中的login方法,也就是调用登录接口
      // console.log(r);
      if(r.code === 0){   //登录成功后会给你返回json数据,里面有code
        //登录成功了
        commit('setUser',r.username)  //修改用户名
        setLocal('token',r.token)   //把得到的token存到localStorage里
      }else{
        // console.log('............');
        return Promise.reject(r.data);  //如果失败,返回一个promise失败态
      }
    },

    //验证token方法
    async validate({commit}){
      let r = await validate(); //调用user.js中的validate方法,也就是调用验证接口
      if(r.code === 0){
        commit('setUser',r.username)
        setLocal('token',r.token) //我们说了,验证通过,或者每次切换路由时,都要重新生成token
      }
      return r.code === 0;  //返回token是否失效,true或者false
    }
  }
})

修改Login.vue中的代码:

<template>
    <div>
        <i-input v-model="username" placeholder="请输入用户名..." style="width: 300px"></i-input>
        <i-button type="primary" @click="login()">登录</i-button>
    </div>
</template>

<script>
import {mapActions} from 'vuex' //使用vuex中的mapActions方法,不会的请参考我的文章vuex的使用方法
export default {
    data(){
        return{
            username:''  //定义一个用户名  
        }
    },
    methods:{
        ...mapActions(['toLogin']), //获取store.js文件中的actions中的toLogin方法
        login(){
            // console.log(this['toLogin'](this.username));
            //使用获取到的toLogin方法
            this['toLogin'](this.username).then(data=>{ //因为toLogin返回的是一个Promise,所以可以.then
                this.$router.push('/')  //登录成功,跳到首页面
            },err=>{
                this.$Message.error(err)
            })
        }
    }
}
</script>

别忘了修改ajaxRequest.js文件,在请求拦截的时候,需要加个头,前面我们写死了,这里,要把token给他,然后每次路由跳转访问页面的时候,都会带上这个头,用来验证:

import {getLocal} from "../libs/local"	//引入

//请求拦截,每次请求时,都要加上一个loading效果
instance.interceptors.request.use((config) => {
	//每次请求时,都给他加一个Authorization头,在JWT验证时要用
	config.headers.Authorization = getLocal('token')
	//第一次请求时,显示loading动画
	if (Object.keys(this.queue).length === 0) {
		store.commit('showLoading')
	}
	this.queue[url] = url;
	return config
})

接着在router.js中设置路由:
哪个页面需要登录后才能访问的话,给这个路由添加meta,假如我的 个人中心页面 需要登录后才能访问,那么我需要修改代码:

{
	path: '/profile',
	name: 'profile',
	component: () => import('./views/Profile.vue'),
	meta:{
		needLogin:true
	}
}

最后修改main.js中的代码,当切换路由时,进行验证:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

//引入iView
import iView from 'iview'
import 'iview/dist/styles/iview.css';

Vue.use(iView)

Vue.config.productionTip = false

//每一次切换路由时,都执行这个导航守卫
router.beforeEach(async (to,from,next)=>{
  let isLogin = await store.dispatch('validate')  //判断是否登录了
  // needLogin  表示哪些路由需要在登录条件下才能访问
  console.log(to);
  let needLogin = to.matched.some(match=>match.meta.needLogin)
  if(needLogin){
    //需要登录
    if(isLogin){
      //登录过了
      next()
    }else{
      //没有登录
      next('/login')
    }
  }else{
    //不需要登录
    if(isLogin && to.path === '/login'){  //如果你访问login页面,则给你跳到首页面,因为不需要登录
      next('/')
    }else{
      next()
    }
  }
})

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

有关需要注意的点,都加注释了,好好看注释就行

至此,整个案例就结束了

源码

点我获取源码


T_T