不就是加个验签嘛,最多两天!啥?前后端不分离?
事情是这样的,前两天,
项目经理找我,“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访问,本身就慢了些,数据库配置的是远程地址。。。更慢,还有什么原因就无从所知,反正一个接口请求访问大概,十五六七八秒!一个页面打开,嚯,十几个接口,于是时间就这么滴滴答答过去了!没事,我一会儿就调完。
然而,各意不断出现,啪啪打脸!
踩坑盘点
-
本以为只处理ajax请求即可,发现除此之外,还有一些a标签跳转,iframe引入,以及各种业务页面内自己添加的js方法生成iframe添加src。因为a标签href和iframe中src配置的都是后台接口,大多都会拼接参数传递给后台用于处理业务逻辑,然后接口中通过转发直接载入前端jsp页面。所以a标签和iframe标签的情况也需要进行拦截处理。
-
原本商定以请求url+参数生成签名信息,但测试过程中发现ajax请求中有的路径写的绝对路径,有的写的相对路径,导致在ajaxSetup中拦截到的url与后台url不一致(后台拦截到的是全路径),导致生成的签名不一致,无法验证通过。因为页面是通过后台重定向或者转发跳转的,通过document.referrer无法获取到真实来源。后面又协商更改了生成密文规则。
-
与后台对接过程中,前后端生成验签不一致的一些场景,如接收到参数排序不一致;base64算法生成签名字符串不一致;正则表达式匹配含特殊字符的参数不参加校验中正则不匹配。
-
其余还有一些后台接收参数过程中formData格式和json格式接收方式不同等。
解决方案
下载sha256.js:github.com/emn178/js-s… ,下载base64.js:github.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请求,所以无需处理。
通过这件事情学到了什么?
- 不要说大话,预估工期一定谨慎摸清需求及实现方案是否可行后再说,另外需要预留安全时间;
- 另外不要轻易碰前后端不分离项目。。。。说到这儿突然想到前段时间一个老项目要开发手机端,领导问我后台是否需要重写,能否直接使用。想都没想满口回答可以直接拿来用,不需要改动。做的时候突然发现老项目是前后端不分离的,接口各种转发和重定向,而非我预期的返回正常的数据😔。为什么都2022年了,我还要处理这些前后端不分离的状况😭。
转载自:https://juejin.cn/post/7139188694065348616