Web请求体数字签名(JS加签、Java验签)
前情
为什么要搞,要这么做? 难道只⽤HTTPS不够吗?
如果应⽤只使⽤HTTPS,那还真不够⽤!
原因:攻击者可以模拟客户端操作,枚举敏感⽤户信息、攻击应⽤。譬如,管理界⾯只要是放在互联⽹中,那么攻击者
就能够通过⽹络直接访问。只要是能访问,那么客户端与服务端的链接通道就到了,并打开了。在数据还没有进⼊到互联⽹环境前,攻击者可利⽤三⽅⼯具对模拟真实的请求,并对其拦截、抓包、修改,如此变绕开了前端的基础校验。
对于⼀些特殊敏感数据,例:⽤户表,主键id(userId)。这些数据如果通过HTTP、互联⽹环境传输到服务器端,⽽恰巧主键⽣成策略是有规律可循(bigint⾃增、某种规律性的公式)的,那么攻击者可以通过枚举的⽅式,⾼频繁修改请求包信息,请求服务端。以此来获取⼀些敏感的数据信息。
思路
攻击者能够肆⽆忌惮的攻击服务器,归根结底是因为两点:
1. 请求被抓包,对包信息修改;
2. 修改后的包信息可以直接发送给服务端;
数字签名的注意事项:
1. 因为是全局性处理,所以必须要考虑性能损耗!
2. 数字签名不能被攻击者复制。否则数字签名就⽆效了!
3. 对传输的请求不要任何脏影响,也就是说请求体数据必须保证完整性!
针对以上思考,采⽤的⽅案:
1. 签名算法使⽤MD5(AES、国密、甚⾄RSA都可以);
2. 考虑到MD5的易破解性,所以我们加slat (必须包含特殊字符,确保安全);
3. 服务端使⽤Filter对请求做合法判断处理;
4. 因为HTTP⽅法有多种,Content-Type存在多种。所以我们采⽤String格式做签名**(保证数据顺序的⼀致性)**;
开始
环境介绍:
1. 前端框架:Vue 4.5.10,使⽤Axios作为⽹络请求库;
2. 包管理⼯具:npm 6.9.0
3. 后端框架:SpringBoot 2.
4.1
前端开发
安装加密组件
# npm 安装加密组件
cnpm install crypto-js
说明:⽤其他组件也可以,或者⾃⼰⼿写都⾏。关键是能保证前后端的验签算法保持⼀致即可。
crypto.js (封装 util)
import CryptoJS from'crypto-js'
/**
* 加盐MD5加密,可以作为加签算法
* @param {加密对象} obj
* @param {*} slat
*/
export function MD5(obj, slat){
if(!obj){
return obj;
}
// 转换成字符串
let str =JSON.stringify(obj);
if(!!slat){
// 拼接slat
str = at(slat)
}
// 关键点:将所有上引号替换成空,理由:后台Filter获取的参数全部为String,所以为了保证格式⼀致,取消掉上引号 str = placeAll(/"/g,"");
// JSON数据存在特殊符号[和]
str = placeAll("\[","");
str = placeAll("]","");
// MD5加密后,转成字符串
return CryptoJS.MD5(str).toString();
}
关键点:
为了保证后端在验签时,对数据的还原保持⼀致性,所以需要对特殊字符做处理(删除)
加盐的公式,我们可以任意⾃定义,不变的是,保证slat具有⼀定的复杂性
Axios全局request拦截
import axios from'axios'
/**
* 需要加签、验签的路径集合
* 例:"/user",将匹配以"/user"开头的所有API
*/
const blackBeginUrl =["/user","/role"]
// HTTP request拦截
quest.use(
(config)=>{
const meta = a ||{}
const isToken = meta.isToken ===false
if(getToken()&&!isToken){
config.headers['Authorization']=getToken()
}
// 判断是否需要对路径做加签操作
let needSign =false;
for(let blackUrl of blackBeginUrl){
if(config.url.indexOf(blackUrl)!=-1){
needSign =true;
break;
}
}
if(needSign){
// 这⾥默认post请求的Content-Type:application/json (可以和开发者做好约定)
let requestData ="";
hod ==="get"&&!!config.params){
requestData = config.params
}else hod =="post"&&!!config.data){
requestData = config.data
}
// 时间戳,作为slat的必备组成之⼀
const timestamp = Date.parse(new Date())
config.headers['Timestamp']= timestamp
// 随机字符串,作为slat的必备组成之⼀
const randomStr ="K:*C8bw6zJ"
// slat = 时间戳 + 随机字符串(⾃定义slat公式)
const slat = at(randomStr);
const signature =MD5(requestData, slat);
config.headers['Signature']= signature;
}
return config;
},
(error)=>{
tryHideFullScreenLoading()
ject(error)
},
)
随机⽣成⽹站:
说明:如果业务上对个别API加签,可以仿照上述代码的⽅式,定义需要验签的API⿊名单。
4. 效果
通过上图可以看到,我们对本次请求成功⽣成了数字签名。Headers key为 Signature。⾄此,前端的⼯作就完成了!
后端
YAML配置
demo:
signature:
stub:
header-signature:"Signature"
header-timestamp:"Timestamp"
# 保证与前端⼀致。这⾥可以做加密处理,防⽌所有开发⼈员都知道
random-str:"K:*C8bw6zJ"
# 需要校验的路径集合
path:
include-url:
replaceall()- /user/*
- /role/*
SignatureCheckFilter.java (核⼼:Filter Logic)
package;
import MapUtil;
import StrUtil;
import SecureUtil;
import Gson;
import GsonBuilder;
import SignatureCheckException;
import SignatureHeaderMissingException;
import SignatureProperty;
import CustomHttpServletRequestWrapper;
import AntPathMatcher;
import ContentType;
import*;
import HttpServletRequest;
import IOException;
import LinkedHashMap;
import Map;
/**
* 签名校验过滤器
*
* @author utrix
* @date 2021/11/18
*/
@Setter
@Component
@ConfigurationProperties(prefix ="demo.signature.stub")
public class SignatureCheckFilter implements Filter {
/**
* 签名Header的key
*/
private String headerSignature;
/**
* 签名校验之时间戳 Header key
*/
private String headerTimestamp;
/**
* 与前端约定的加签随机值
*/
private String randomStr;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException { HttpServletRequest servletRequest =(HttpServletRequest) request;
// ⽂件上传类的直接放⾏
if(contentType ==null|| contentType.startsWith(ContentType.MULTIPART_MimeType())){
chain.doFilter(request, response);
return;
}
HttpServletRequest requestWrapper = servletRequest;
// 获取请求体的数据
String requestData;
String contentType = ContentType();
// 获取Content-Type为 JSON格式的请求体数据
if(StrUtil.isNotEmpty(contentType)&& ains("application/json")){
// 因为Reader() 或者InputStream()只能读取⼀次,所以我们需要⾃定义包装类
requestWrapper =new CustomHttpServletRequestWrapper(servletRequest);
requestData =((CustomHttpServletRequestWrapper) requestWrapper).getRequestData();
}else{
// GET类请求数据
Map<String, String[]> parameterMap = ParameterMap();
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论