基于 Vue-cli 实现 HashHistory 模式的 URL 参数加密

2,184 阅读3分钟

写在前面

路由组件之间传参,是实现不同页面之间获取数据的途径之一。Vue-router 可以通过对特定的 paramsquery 属性设置,URL 即可直接拼上相应的数据并实现页面直接数据传输,但如果有些数据比较敏感,或者不允许直接修改 URL 上的参数,来获得不被允许的内容信息时,URL 数据明文显示就无法得到保障,这个时候我们就需要对URL相应的参数进行加密。

URL 参数加密实现的两种方式

想要实现 URL 参数加密、解密,主要是不希望数据直接明文显示在 URL 上,同时也不允许修改数据而获得不被允许的信息。基于 Vue-cli,可以通过两种方式(只是本人认为的两种,还可以有很多方式):一种是局部参数加密,在传输数据前,会先通过加密函数对需要传递参数数据进行加密,获取数据时再通过解密函数进行解密即可;一种是全局参数加密,再全局前置守卫时对参数继续加密传递,获取数据时,可直接获得无加密的数据。

下面会详细介绍第二种方式 -- 全局参数加密思路。

了解 Vue-routerHashHistory 模式

HashHistory 的路由替换是通过两种方式:一种是 push,一种是 replace

Push 是将新访问的路由添加到浏览器访问历史栈顶,而 replace 是替换当前路由。

以下是 push 方式从设置路由改变到视图更新的流程:


$router.push() -> HashHistory.push() -> History.transitionTo() -> History.updateRoute() -> { app._route = route } -> vm.render()

解析:


-> $router.push() //路由切换,调用方法

-> HashHistory.push() //根据 hash 模式调用,设置 hash 并添加在浏览器历史记录(window.location.hash = xx)

-> History.transitionTo() // 监听更新,更新则调用 History.updateRoute()

-> History.updateRoute() // 更新路由

-> { app._route = route } //替换当前 app 路由

-> vm.render() //更新路由

实现全局 URL 参数加密思路

了解完 Vue-routerHashHistory 模式后,并参考 Vue-router 源码了解,我们知道:

  1. 路由切换都会调用 push 方法;

  2. 在路由第一次进入加载时,是不会调用 push 方法的;

  3. $routerVueRouter 的实例对象,包含所有路由信息,对象以及方法,而 $route 是当前路由信息对象,可以获取到当前路由的 namepathqueryparams 等参数值;

结合我们 URL 参数加密的需求,可以做出相应的解决方案:

  1. 基于 Vue-clinew Vue 实例对象进行全局修改;
  2. new Vue 实例对象中 _router(即 $router) 对象 (也就是 Vue-router 的实例对象)的 push 方法进行修改,对所有 URL 参数进行加密;
  3. 通过 Vue-router 的全局前置守卫,处理路由第一次加载事件和不需要加密的 URL;
  4. 通过 Object.definePropertynew Vue 实例对象的 _routerRoot (根vue实例)属性进行劫持,重写 get 方法,对 _route(即 $route )的相应加密参数数据进行解密。以下为源码说明;

  // new Vue 时或者创建新组件时,在 beforeCreate 钩子中调用
  Vue.mixin({
    beforeCreate() {
      if (isDef(this.$options.router)) {  // 组件是否存在$options.router,该对象只在根组件上有
        this._routerRoot = this           // 这里的this是根vue实例
        this._router = this.$options.router      // VueRouter实例
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else { // 组件实例才会进入,通过$parent一级级获取_routerRoot
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed() {
      registerInstance(this)
    }
  })
  

部分代码实现与解析

  • new Vue 实例对象(以下命名为 “VO”)中 _router(即 $router) 对象 (也就是 Vue-router 的实例对象)的 push 方法进行修改,对所有 URL 参数进行加密;通过 Object.create 创建一个新的对象,使用 VO 来提供新创建的对象的 __proto__,来实现对 push 方法进行修改,对所有参数值进行加密,同时在末尾加上 “JM” 加密标识
    // VO 为 new Vue 实例对象
    let NVO = Object.create(VO);  

    NVO._router.newPush = NVO._router.push;  
    NVO._router.push = function push (location, onComplete, onAbort) {
        // 给全部 url 参数加密    
        // encryUrl 为加密函数,可以使用 window.btoa、window.atob 或者 使用 crypto-js    
        let params = encryUrl(location.params);   
  
        NVO._router.newPush.call(VO._router, Object.assign({}, location, { params }), onComplete, onAbort);  
    };

encryUrl 加密方法:

const CryptoJS = require('crypto-js');  //引用AES源码js    
const key = CryptoJS.enc.Utf8.parse('1234123412ABCDEF');  //十六位十六进制数作为密钥
const iv = CryptoJS.enc.Utf8.parse('ABCDEF1234123412');   //十六位十六进制数作为密钥偏移量

//加密方法
function Encrypt(params) {

    //获取入参对象的第一个 key  
    const key = Object.keys(params)[0];

    if (!key) {
        return params;
    }  

    //获取入参对象的第一个 key  
    const key = Object.keys(params)[0];

    let srcs = CryptoJS.enc.Utf8.parse(params[key]);  
    let encrypted = CryptoJS.AES.encrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); 
     
    //末尾添加的'JM'表示加密标识 
    return Object.assign({}, params, { [key]: `${encrypted.ciphertext.toString().toUpperCase()}JM` });
}

  • 通过 Vue-router 的全局前置守卫,处理路由第一次加载事件和不需要加密的 URL;进入路由前,都会触发 beforeEach 全局前置守卫方法,此时对不需要加密的路由参数数据进行解密,并重新加载;同时路由如果是第一次加载进入,直接判断参数值是否已经加密,如果是加密则直接 next(),反之则在参数值上加上特殊符号 "+00",实现前端拦截, URL 参数值不允许手动修改;

  NVO._router.beforeEach((to, from, next) => {
    //获取入参对象的第一个 key
    const key = Object.keys(to.params)[0];

    if (key) {
      //判断 url 参数是否需要加密
      if (to.matched.some(record => record.meta.encryUrl)) { // url 参数需要加密
        if (isEncry(to.params[key])) {
          next();
        } else {  // url 参数没有加密
          // url 参数没有加密,直接通过参数进入页面时,需加上特殊符号 '+00',实现前端拦截
          next({
            name: to.name,
            params: encryUrl(Object.assign({}, to.params, { [key]: `${to.params[key]}+00` })),
            query: to.query
          }); 
        }
      } else { // url 参数不需要加密
        if (isEncry(to.params[key])) { //url 参数已加密
          // 由于在 push 方法时,都统一加密了,所以这里需要解密
          const value = replaceEncryTag(to.params[key]);

          next({
            name: to.name,
            params: {
              [key]: decryptUrl(value)
            },
            query: to.query
          }); 
        } else { //url 参数未加密
          // 直接访问
          next();
        }
      }
    } else {
      // 直接访问
      next(); 
    }
  });

  • 通过 Object.definePropertynew Vue 实例对象的 _routerRoot (根vue实例)属性进行劫持,重写 get 方法,对 _route(即 $route )的相应加密参数值,去掉 “JM” 加密标识后,进行解密,并返回 _routerRoot 对象即可;