接口安全:防篡改和防重放
API
作为应用程序之间的桥梁,承载了大量的数据交换任务。然而,那些暴露在互联网中的接口也可能成为攻击者的目标。为了确保数据传输的安全性,我们必须采取有效的安全措施来防范篡改攻击和重放攻击。下面我们将以 Spring Boot
应用中的 API
安全设计为例,讲解下如何实施这些安全措施。
什么是篡改攻击和重放攻击
篡改攻击
篡改攻击利用了数据在传输过程中的不安全性。攻击者可以通过中间人攻击(Man-in-the-Middle, MITM
)等方式拦截数据包,然后修改其中的关键字段,比如金额、用户身份信息等,再将修改后的数据包发送给接收方,以达到恶意目的。这种攻击可以发生在客户端到服务器之间的任何点,尤其是在没有加密或加密强度不足的情况下。
比如:攻击者截获用户认证请求,可以修改其认证信息,冒充合法用户登录系统。
重放攻击
重放攻击是指攻击者拦截并重新发送之前有效的数据包,以达到重复执行某项操作的目的。这种攻击利用了系统的状态管理和时间敏感性不足的问题。重放攻击依赖于数据包的有效性和可重复性。攻击者截获一个有效的数据包(如登录请求),然后在适当的时候再次发送这个数据包,从而绕过系统的身份验证或其他控制机制。
比如:攻击者截获一次成功的转账请求,然后多次重放该请求,导致用户账户被多次扣款。
防御大法
HTTPS
(本文不做详细赘述)HTTPS
通过SSL/TLS
协议对数据进行加密,确保数据在传输过程中不被窃听或篡改。此外,HTTPS
还提供了身份验证机制,确保通信双方的身份。
基于数字签名防篡改
- 数字签名基于密钥的加密技术。
- 发送方使用使用约定好的密钥对传输参数进行签名,生成一个签名值,并将签名值放入请求
header
中; - 接收方使用约定的密钥对请求参数再次进行签名;
- 接收方对两次签名的值进行对比,对比一致则认为请求合法,不一致则说明请求被篡改。
基于时间戳防重放
- 时间戳是一种确保数据新鲜度的方法。每个请求中加入时间戳,并在服务器端检查时间戳的新鲜度。时间戳确保请求在一定时间内是有效的。服务器收到请求后,会检查时间戳是否在允许的时间范围内。如果时间戳过期,则拒绝请求。
- 发送方每次请求都在请求
header
中放入时间戳参数,并且时间戳要和传输参数一起进行数字签名; - 接收方收到请求后,首先取请求
header
中的时间戳并与当前时间进行比较,如果时间差超过了预设的阈值,则认为签名过期。
签名验签编码思路
进行签名验签的 Header 信息:
参数 | 类型 | 说明 |
---|---|---|
X-Signature | header | 请求的签名值 |
X-Timestamp | header | 请求时的时间戳 |
X-Algorithm | header | 签名时使用的算法 |
签名验签的数据部分:
- 时间戳:
header
中X-Timestamp
的值; Query
参数:Query
请求参数,例如request?username=aaa&age=18
,多个Query
参数需要对key
按字典(ASCII
码)升序排序后,再按照value1+value2
方法拼接;Body
数据:实际的请求体内容。
过滤器编码如下:
@Slf4j
public class SignatureCheckFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException {
CacheRequestBodyWrapper requestWrapper = new CacheRequestBodyWrapper(request);
// 获取请求头中的签名和时间戳
String signature = requestWrapper.getHeader("X-Signature");
String timestampStr = requestWrapper.getHeader("X-Timestamp");
String algorithm = requestWrapper.getHeader("X-Algorithm");
if (Objects.isNull(signature) || Objects.isNull(timestampStr)) {
log.warn("Missing required headers");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing required headers");
return;
}
// 重放时间限制
long timestamp = Long.parseLong(timestampStr);
if (System.currentTimeMillis() - timestamp >= 60 * 1000) { // 这里写死60s,实际可做配置
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Signature has been expired.");
return;
}
// 获取 query 请求字符串
String requestQuery = getRequestQueryStr(requestWrapper);
// 获取 body 数据
String body = getRequestBody(requestWrapper);
// 按照规则进行签名
String signData = timestamp + requestQuery + body;
String newSignature = DigestUtils.getSignature(signData, algorithm, "UTF-8");
log.info("计算出的新的签名值:----------->>>>>> {}", newSignature);
log.info("header里面的签名值:---------->>>>>> {}", signature);
if (!newSignature.equals(signature)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Signature verification failed");
return;
}
filterChain.doFilter(requestWrapper, response); // 注意这里传递的是 cachedRequest
}
/**
* 获取请求 body
*
* @param request HttpServletRequest
* @return 请求 body 字符串
*/
private String getRequestBody(CacheRequestBodyWrapper request) throws IOException, UnsupportedEncodingException {
StringBuilder sb = new StringBuilder();
BufferedReader reader = request.getReader();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
/**
* 获取 query 请求字符串
*
* @param request HttpServletRequest
* @return 请求字符串
*/
private String getRequestQueryStr(HttpServletRequest request) {
List<String> reqList = new ArrayList<>();
Enumeration<String> reqEnu = request.getParameterNames();
while (reqEnu.hasMoreElements()) {
reqList.add(reqEnu.nextElement());
}
Collections.sort(reqList);
StringBuilder requestQuery = new StringBuilder();
for (String key : reqList) {
String value = request.getParameter(key);
if (value != null) {
requestQuery.append(value);
}
}
log.info("获取的query请求字符串是:------>>> {}", requestQuery);
return requestQuery.toString();
}
}
注册过滤器:
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<SignatureCheckFilter> signatureCheckFilter(){
FilterRegistrationBean<SignatureCheckFilter> registrationBean = new FilterRegistrationBean<>();
SignatureCheckFilter filter = new SignatureCheckFilter();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns("/xxx/xxxxxx/*"); // 指定需要处理的url
registrationBean.setOrder(1);
return registrationBean;
}
实现防篡改和防重放攻击的方式有很多种,本文只是简单介绍了基于数字签名和时间戳的实现方案,并提供了一个编码思路。希望通过这些内容,大家可以有所启发,并能够在实际项目中灵活运用这些安全措施。
【Java驿站】持续给大家更新,扫描下方👇👇👇二维码,关注【Java驿站】公众号
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。