接口安全:防篡改和防重放

文摘   2024-11-14 15:14   山东  
API作为应用程序之间的桥梁,承载了大量的数据交换任务。然而,那些暴露在互联网中的接口也可能成为攻击者的目标。为了确保数据传输的安全性,我们必须采取有效的安全措施来防范篡改攻击和重放攻击。本文将以Spring Boot应用中的API安全设计为例,详细讲解如何实施这些安全措施。

什么是篡改攻击和重放攻击

篡改攻击

篡改攻击利用了数据在传输过程中的不安全性。攻击者可以通过中间人攻击(Man-in-the-Middle, MITM)等方式拦截数据包,然后修改其中的关键字段,比如金额、用户身份信息等,再将修改后的数据包发送给接收方,以达到恶意目的。这种攻击可以发生在客户端到服务器之间的任何点,尤其是在没有加密或加密强度不足的情况下。

比如:攻击者截获用户认证请求,可以修改其认证信息,冒充合法用户登录系统。

重放攻击

重放攻击是指攻击者拦截并重新发送之前有效的数据包,以达到重复执行某项操作的目的。这种攻击利用了系统的状态管理和时间敏感性不足的问题。重放攻击依赖于数据包的有效性和可重复性。攻击者截获一个有效的数据包(如登录请求),然后在适当的时候再次发送这个数据包,从而绕过系统的身份验证或其他控制机制。

比如:攻击者截获一次成功的转账请求,然后多次重放该请求,导致用户账户被多次扣款。
防御大法

  • HTTPS(本文不做详细赘述)

    • HTTPS通过SSL/TLS协议对数据进行加密,确保数据在传输过程中不被窃听或篡改。此外,HTTPS还提供了身份验证机制,确保通信双方的身份。

  • 基于数字签名防篡改

    • 数字签名基于密钥的加密技术。

    • 发送方使用使用约定好的密钥对传输参数进行签名,生成一个签名值,并将签名值放入请求header中;

    • 接收方使用约定的密钥对请求参数再次进行签名;

    • 接收方对两次签名的值进行对比,对比一致则认为请求合法,不一致则说明请求被篡改

  • 基于时间戳防重放

    • 时间戳是一种确保数据新鲜度的方法。每个请求中加入时间戳,并在服务器端检查时间戳的新鲜度时间戳确保请求在一定时间内是有效的。服务器收到请求后,会检查时间戳是否在允许的时间范围内。如果时间戳过期,则拒绝请求。

    • 发送方每次请求都在请求header中放入时间戳参数,并且时间戳要和传输参数一起进行数字签名;

    • 接收方收到请求后,首先取请求header中的时间戳并与当前时间进行比较,如果时间差超过了预设的阈值,则认为签名过期。

签名验签编码思路

进行签名验签的Header信息

参数
类型说明
X-Signature
header请求的签名值
X-Timestampheader请求时的时间戳
X-Algorithmheader签名时使用的算法
签名验签的数据部分:
  1. 时间戳:header中X-Timestamp的值;

  2. Query参数:Query请求参数,例如request?username=aaa&age=18,多个Query参数需要对key按字典(ASCII码)升序排序后,再按照value1+value2方法拼接;

  3. Body数据:实际的请求体内容。
过滤器编码如下:
@Slf4jpublic 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(); }}

注册过滤器:

@Configurationpublic 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驿站】,一个Java编程学习与交流平台。
 最新文章