前言

在使用Vue框架时,我们通常使用axios收发服务器的请求。为了避免诸多恶意行为,我们必须使用axios拦截器进行屏蔽。下面我将以axios后置拦截器结合用户登录举例说明


一、逻辑

1. 用户输入邮箱和密码(已经注册);
2. 邮箱错误,弹出提醒,并拒绝执行后续操作;
3. 密码错误,弹出提醒,并拒绝执行后续操作;
4. 邮箱和密码均正确时,放行,跳转到后台界面。

二、后端部分

controller

代码如下:

@PostMapping("/login")
    public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response)
    {
        User user = userService.getOne(new QueryWrapper<User>().eq("email",loginDto.getEmail()));
        Assert.notNull(user,"用户不存在!");//断言user不为空,为空则抛出message

        if (!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))) {
            return Result.fail("密码不正确");
        }

        String jwt = jwtUtils.generateToken(user.getId());

        response.setHeader("Authorization",jwt);
        response.setHeader("Access-control-Expose-Headers","Authorization");
        user.setLastLogin(LocalDateTime.now());
        userService.updateById(user);

        return Result.success(MapUtil.builder()
                .put("id",user.getId())
                .put("username",user.getUsername())
                .put("avatar",user.getAvatar())
                .put("email",user.getEmail())
                .map()
        );

上述代码可以简单理解为3个部分:

  1. 将前端传回email与数据库对应字段对比,若无相同email,说明用户不存在,直接使用断言抛出错误;
  2. 到了这一步,说明email正确,继续对比密码password,如果不正确,这返回一个Result类型数据(这里Result是我定义的一个统一封装的结果类,分为正确和错误情况,错误情况状态码简单设置为400。给予前端状态和信息反馈),内容如下:
@Data
public class Result implements Serializable {

    private int code;//200正常,非200表示异常
    private String msg;
    private Object data;

    public static Result success(Object data)
    {
        return success(200,"操作成功",data);
    }

    public static Result success(int code, String msg, Object data)
    {
        Result result = new Result();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }

    public static Result fail(String msg)
    {
        return fail(400,msg,null);
    }

    public static Result fail(String msg, Object data)
    {
        return fail(400,msg,data);
    }

    public static Result fail(int code, String msg, Object data)
    {
        Result result = new Result();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }
}
  1. email和password均正确,直接返回Result类的success方法。

2.前端部分

在main.js引入ElementUI、axios(两者需要使用npm进行安装)和axios.js代码如下:

import Element from 'element-ui'
import "element-ui/lib/theme-chalk/index.css"
import axios from 'axios'//引入axios库
import "./axios"//引入axios.js
Vue.prototype.$axios = axios
Vue.use(Element)

axios.js为axios拦截器核心代码,示例如下:

import axios from "axios";
import Element from 'element-ui'
import router from './router'
import store from './store'

axios.defaults.baseURL="http://localhost:8081"

//后置拦截
axios.interceptors.response.use(response => {
    let res = response.data;
    console.log("===========")
    console.log(res)
    console.log("===========")

    if (res.code === 200) {
        return response
    }else {
        Element.Message.error("密码错误,请重新输入!",{duration: 3 * 1000})//持续3秒消失

        return Promise.reject(response.data.message)
    }
    },
    error => {
        if(error.response.data)
        {
            error.message = error.response.data.msg
        }

        if (error.response.status === 401){
            store.commit("REMOVE_INFO")
            router.push("/login")
        }

        Element.Message.error(error.message,{duration: 3 * 1000})

        return Promise.reject(error)
    }
    )

这里引入了axios、elementUI、以及Vue路由router和存储store
其后置拦截器分为响应和错误两种情况,即向后端发起请求时,根据后端状态分别执行不同操作。具体为:
1.后端正常执行,则会返回一个Result类型的数据,执行response流程。我们拿到数据后,获取到其中的状态码code,若email和password正确,返回code200(我将正确状态码设置为200)故当其状态码为200时,直接放行,返回响应,继续执行后续操作;反之,使用ElementUi消息组件进行通知。并使用Promise.reject()方法拒绝执行后续操作。
2.后端执行异常,即Assert断言抛出异常,进入error流程。接着判断能否拿到错误响应数据,能的话将其中错误信息赋值给error.message。由于我使用shiro框架进行登录效验,对于未登录访问需要登录才能访问的接口的行为均会抛出401异常。故对这种情况进行处理。即移除浏览器缓存信息,同时跳转到登录页。最后,同样使用Promise.reject()方法拒绝执行后续操作。

前端配置

store下index.js的全局参数配置(保存jwt和userInfo到浏览器以及将他们移除):

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
/**
 * 全局配置参数
 */
export default new Vuex.Store({
  state: {
    token:localStorage.getItem("token"),
    userInfo:JSON.parse(sessionStorage.getItem("userInfo"))//反序列化获得userInfo对象
  },
  mutations: {
      //set
    SET_TOkEN: (state, token) => {
      state.token = token
      //浏览器关闭时,可以存到浏览器的localStore
      localStorage.setItem("token",token)
    },
    SET_USER_INFO: (state, userInfo) => {
      state.userInfo = userInfo
      //浏览器关闭时,可以存到sessionStorage,但其不能存一个对象,只能序列化存储
      sessionStorage.setItem("userInfo",JSON.stringify(userInfo))
    },
    REMOVE_INFO: (state) => {
      state.token = ''
      state.userInfo = {}
      //移除存储在浏览器缓存的数据
      localStorage.removeItem("token")
      sessionStorage.removeItem("userInfo")
    },
  },
  getters: {
    //get
    getUsers : state => {
      return state.userInfo
    }

  },
  actions: {
  },
  modules: {
  }
})

测试页面

以登录页进行测试(默认以及传入email和password方便测试),代码如下:

<template>
  <div id="login" data-title="登录">
    <!--<video preload="auto" class="me-video-player" autoplay="autoplay" loop="loop">
          <source src="../../static/vedio/sea.mp4" type="video/mp4">
      </video>-->

    <div class="me-login-box me-login-box-radius">
      <h1>用户登录</h1>

      <el-form ref="userForm" :model="userForm" :rules="rules">
        <el-form-item prop="email">
          <el-input placeholder="邮箱" v-model="userForm.email"></el-input>
        </el-form-item>

        <el-form-item prop="password">
          <el-input placeholder="密码" type="password" v-model="userForm.password"></el-input>
        </el-form-item>

        <el-form-item size="small" class="me-login-button">
          <el-button type="primary" @click="login('userForm')">登录</el-button>
        </el-form-item>
      </el-form>



    </div>
  </div>
</template>

<script>
  export default {
    name: 'Login',
    data() {
      return {
        userForm: {
        },
        rules: {
          email: [
            {required: true, message: '请输入邮箱', trigger: 'blur'},
            {type: 'email', message: '邮箱格式不正确', trigger: 'blur, change'}
          ],
          password: [
            {required: true, message: '请输入密码', trigger: 'blur'},
            {max: 20, message: '不能大于20个字符', trigger: 'blur'}
          ]
        }
      }
    },
    methods: {
      login(formName) {

        this.$refs[formName].validate((valid) => {
          if (valid) {
            const _this = this;
            this.$axios.post('/user/login',this.userForm).then(res => {

              const jwt = res.headers['authorization']
              const userInfo = res.data.data;


              //提交存储jwt和userInfo,为了后续页面获取到数据
              _this.$store.commit("SET_TOkEN",jwt)
              _this.$store.commit("SET_USER_INFO",userInfo)


              _this.$message.success("登录成功!")
              //页面跳转
              _this.$router.push("/userIndex")

            })
          } else {
            console.log('error submit');
            return false;
          }
        });
      }
    }
  }
</script>

<style scoped>
  #login {
    min-width: 100%;
    min-height: 100%;
  }

  .me-video-player {
    background-color: transparent;
    width: 100%;
    height: 100%;
    object-fit: fill;
    display: block;
    position: absolute;
    left: 0;
    z-index: 0;
    top: 0;
  }

  .me-login-box {
    position: absolute;
    width: 300px;
    height: 260px;
    background-color: white;
    margin-top: 150px;
    margin-left: -180px;
    left: 50%;
    padding: 30px;
  }

  .me-login-box-radius {
    border-radius: 10px;
    box-shadow: 0px 0px 1px 1px rgba(161, 159, 159, 0.1);
  }

  .me-login-box h1 {
    text-align: center;
    font-size: 24px;
    margin-bottom: 20px;
    vertical-align: middle;
  }

  .me-login-design {
    text-align: center;
    font-family: 'Open Sans', sans-serif;
    font-size: 18px;
  }

  .me-login-design-color {
    color: #5FB878 !important;
  }

  .me-login-button {
    text-align: center;
  }

  .me-login-button button {
    width: 100%;
  }

</style>

测试结果

  1. 用户名错误
  2. 密码错误
  3. 均正确