likes
comments
collection
share

不就是加个验签嘛,最多两天!啥?前后端不分离?

作者站长头像
站长
· 阅读数 21

事情是这样的,前两天,

项目经理找我,“XX,我们A项目数据传输要添加验签,避免恶意篡改参数,加强数据安全,能实现吗?A项目现在着急验收回款,就卡在安全检查上了。”

我:“我记得咱A项目是基于jQuery前后端不分离的,Ajax请求也是大家自己实现的,没有封装公共请求方法。好在项目每个页面都引入了一个common.js,可以在公共js里面进行公共拦截处理。”

项目经理:“你预计几天能做完?”

我:“2天吧,前后端得联调,完了还要发布测试,重新进行安全扫描。”内心想,嗯,一天搞完一天测,还有摸鱼时间呢😏。

到这里,相信大家都能想到,事情并非想象的那么简单,不然我干嘛哭晕倒厕所后又振作起来写这篇文章呢😭?

背景

A项目底层框架是2017年搭建的,当时做的时候不排除使用了以前其他人搭建的前后端框架的可能。前端是bootstrap+jQuery+layui的某些功能组件,基于iframe的多页面应用,路由跳转为后端接口控制。所以,要考虑的情况绝不会只有ajax请求。所以,是什么让我一拍脑袋认为只需要处理ajax请求?因为项目要兼容ie低版本浏览器,所以以下实现多采用es5语法。

这两天我做了什么

第一天上午,后端同事帮我安装idea开发工具,另外Java环境由于电脑文件丢失,需要重新配置。同事问我要不要给我拷贝个JDK?我说我先自己搞下吧(我为什么非想先自己搞?我怕麻烦人家。。。怕什么来什么)。于是去Oracle官网下载最新的JDK工具包,配置好环境变量。

然后同事过来帮我配置idea,。。。。。。。一段时间后,项目启动报错,同事说可能是JDK版本不匹配,拷贝了自己的JDK包来重新一顿配置。。。。。。。。。😅,终于好了,项目启动啦!!!开工吧,一上午就这么过去了。。。。。。。😔唉,时间过得可真快,倒计时还剩一天半,没关系,我还有时间!

下午开工,项目可真(文明用语)慢!需要连接VPN访问,本身就慢了些,数据库配置的是远程地址。。。更慢,还有什么原因就无从所知,反正一个接口请求访问大概,十五六七八秒!一个页面打开,嚯,十几个接口,于是时间就这么滴滴答答过去了!没事,我一会儿就调完。

然而,各意不断出现,啪啪打脸!

踩坑盘点

  1. 本以为只处理ajax请求即可,发现除此之外,还有一些a标签跳转,iframe引入,以及各种业务页面内自己添加的js方法生成iframe添加src。因为a标签href和iframe中src配置的都是后台接口,大多都会拼接参数传递给后台用于处理业务逻辑,然后接口中通过转发直接载入前端jsp页面。所以a标签和iframe标签的情况也需要进行拦截处理。

  2. 原本商定以请求url+参数生成签名信息,但测试过程中发现ajax请求中有的路径写的绝对路径,有的写的相对路径,导致在ajaxSetup中拦截到的url与后台url不一致(后台拦截到的是全路径),导致生成的签名不一致,无法验证通过。因为页面是通过后台重定向或者转发跳转的,通过document.referrer无法获取到真实来源。后面又协商更改了生成密文规则。

  3. 与后台对接过程中,前后端生成验签不一致的一些场景,如接收到参数排序不一致;base64算法生成签名字符串不一致;正则表达式匹配含特殊字符的参数不参加校验中正则不匹配。

  4. 其余还有一些后台接收参数过程中formData格式和json格式接收方式不同等。

解决方案

下载sha256.jsgithub.com/emn178/js-s… ,下载base64.jsgithub.com/dankogai/js… ,公共jsp页面引入,common.js添加ajax全局拦截,跟后端商定为便于前台添加和后台拦截,将密文参数放于headers头信息中。

  • 前端使用sha256算法根据请求生成签名,通过jQuery.ajaxSetup()函数设置AJAX的全局默认headers头信息,插入密文信息传递给后台。
  • 前台往后台发送请求,后台检查是否携带参数?没有放行。如果携带了参数看有无验签信息?无验签信息拦截不允许访问,有验签信息比对与后端生成的是否一致?一致则放行,不一致则拦截不允许访问。

生成加密参数:

//是否是json对象
function isJsonObj(obj) {
    return typeof obj === 'object' &&
        Object.prototype.toString.call(obj).toLowerCase() === '[object object]' &&
        !obj.length
}

//获取url拼接的参数
function getParams(str){
    let theRequest = {};
    var strs = str.split("&");
    for (var i = 0; i < strs.length; i++) {
        theRequest[strs[i].split("=")[0]] = strs[i].split("=")[1];
    }
    return theRequest;
}

//生成签名
var createSignFun = function(options){
    var url = options.url;
    if(!url) return false;
    // 验签主逻辑
    // 参数json
    let bodyJson = {}

    // 数字签名密钥
    var appSecret = 'xxxxxx';//约定的公钥
    var data = options.data || '';
    //或url拼接后面的参数,主要针对于get请求
    if(url.split('?')[1]){
        data = getParams(url.split('?')[1]);
    }
    bodyJson = data ? data : bodyJson;
    bodyJson.signParam1 = Math.random()//生成随机数,此处更换为randomjs
    bodyJson.signParam2 = Date.now()//生成时间戳
    var keys = Object.keys(bodyJson)
    var arr = []
    //处理成字符串
    for (var i in keys) {
        if (
            bodyJson[keys[i]] !== null &&
            bodyJson[keys[i]] !== '' &&
            JSON.stringify(bodyJson[keys[i]]) !== '{}' &&
            JSON.stringify(bodyJson[keys[i]]) !== '[]' &&
            keys[i] !== 'upload'
        ) {
            if (
                isJsonObj(bodyJson[keys[i]]) ||
                Array.isArray(bodyJson[keys[i]])
            ) {
                arr.push(`${keys[i]}=${JSON.stringify(bodyJson[keys[i]])}`)
            } else {
                arr.push(`${keys[i]}=${bodyJson[keys[i]] + ''}`)
            }
        }
    }
    if (arr.length > 0) {
        // 正则表达式匹配特殊字符,包含特殊字符的参数不参加验签,避免传输过程中丢失
        var lastIndex = url.lastIndexOf('/');
        url = lastIndex >= 0 ? url.slice(lastIndex,url.length) : '/'+url;
        var pattern = new RegExp('[\\xxxxxx]')//与后台约定好的不参加验签的特殊字符
        // 按字典序排序
        arr.sort()
        // 添加url作为一级参数
        var mingStr =
            url
                .split('?')[0]
                .replace(/[\r\n]/g, '')
                .replace(/\s+/g, '') + '?'
        for (let i = 0; i < arr.length; i++) {
            if (pattern.test(arr[i])) {
                continue
            }
            if (i < arr.length - 1) {
                mingStr = `${mingStr + arr[i]}&`
            } else {
                mingStr += arr[i]
            }
        }
        // 生成签名字符串并附加到请求参数里
        const base64Str = Base64.encode(appSecret + encodeURIComponent(mingStr));
        bodyJson.signParam3 = sha256(base64Str + appSecret)
            .toString()
            .toUpperCase()
    }
    return bodyJson
}

公共ajax请求拦截

$.ajaxSetup({
    beforeSend:function(jqXHR, settings){
        if(settings.url.indexOf('signature') < 0){
        //createSignFun用于处理参数生成验签参数,并拼接到ajax请求头中
            var signInfo = createSignFun(settings);
            jqXHR.setRequestHeader('signParam1', signInfo.signParam1);
            jqXHR.setRequestHeader('signParam2', signInfo.signParam2);
            jqXHR.setRequestHeader('signParam3', signInfo.signParam3);
        }

    },
})

iframe中处理添加签名:遍历页面中所以iframe,如果url中没有添加签名信息,则在url中拼接签名,a标签同理

function filterUrl(url) {
  //处理路径添加验签,如果没有参数则不更改,含有验签信息则不更改
  if (!url || url.indexOf('?') < 0 || url.indexOf('signature') > 0)
    return false;
  var signInfo = createSignFun({ url: url });
  var tempParam =
    'signParam1=' +
    signInfo.signParam1 +
    '&signParam2=' +
    signInfo.signParam2 +
    '&signParam3=' +
    signInfo.signParam3;
  console.error(url, ':filterUrl处理路径添加验签');
  return url.indexOf('?') > 0
    ? url + '&' + tempParam
    : url + '?' + tempParam;
}

$(window.parent.document)
  .find('iframe')
  .each(function () {
    var str = $(this).attr('src');
    if (filterUrl(str)) {
      $(this).attr('src', filterUrl(str));
    }
  });

另外还有通过.load()方法载入页面的,本来以为需要单独处理,还单独重写了.load()方法载入页面的,本来以为需要单独处理,还单独重写了.load().load()方法,结果发现jqeury内部已经处理过了,load也是走了ajax请求,所以无需处理。

通过这件事情学到了什么?

  1. 不要说大话,预估工期一定谨慎摸清需求及实现方案是否可行后再说,另外需要预留安全时间;
  2. 另外不要轻易碰前后端不分离项目。。。。说到这儿突然想到前段时间一个老项目要开发手机端,领导问我后台是否需要重写,能否直接使用。想都没想满口回答可以直接拿来用,不需要改动。做的时候突然发现老项目是前后端不分离的,接口各种转发和重定向,而非我预期的返回正常的数据😔。为什么都2022年了,我还要处理这些前后端不分离的状况😭。
转载自:https://juejin.cn/post/7139188694065348616
评论
请登录