一、实现防重放攻击的方案

  • 使用Nonce和Timestamp: 在请求中加入一个随机数(Nonce)和当前时间戳,服务端收到请求后验证该请求的Nonce是否已经使用过并且请求的时间戳是否在合理时间范围内。

  • 使用Token: 在服务端生成一个唯一的Token,并在响应中返回给客户端,客户端在下一次请求中需要带上该Token,服务端收到请求后验证该Token是否已经使用过。

  • 使用Redis 或者 JWT: 将每次请求的信息存储在Redis中或者JWT中,服务端收到请求后验证该请求是否重复。

二、使用Nonce和Timestamp进行防重放攻击

具体实现方式如下:

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * @author Clang
 * @version 1.0
 * @date 2023/1/17 14:01
 */
@Component
@Slf4j
public class ReplayAttackFilter implements GlobalFilter, Ordered {

    private static final long TIMESTAMP_VALID_TIME = 5 * 60 * 1000;

    private final Set<String> usedNonceSet = Collections.synchronizedSet(new HashSet<>());

    /**
     * 每分钟执行一次
     */
    @Scheduled(cron = "0 * * * * *")
    public void clearUsedNonceSet() {
        usedNonceSet.clear();
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 从请求头中获取Nonce和Timestamp
        String nonce = exchange.getRequest().getHeaders().getFirst("nonce");
        String timestamp = exchange.getRequest().getHeaders().getFirst("Timestamp");
        // 验证Nonce和Timestamp是否合法
        if (validateNonceAndTimestamp(nonce, timestamp)) {
            // 如果合法,则放行请求
            return chain.filter(exchange);
        } else {
            // 如果不合法,则返回错误响应
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
    }

    @Override
    public int getOrder() {
        // 设置过滤器的优先级
        return -1;
    }

    private boolean validateNonceAndTimestamp(String nonce, String timestamp) {
        // 判断Nonce和Timestamp是否为空
        if (nonce == null || timestamp == null) {
            return false;
        }

        // 验证Nonce是否已经使用过
        if (usedNonceSet.contains(nonce)) {
            return false;
        } else {
            usedNonceSet.add(nonce);
        }

        // 验证Timestamp是否在合理时间范围内
        long timeStampValue;
        try {
            timeStampValue = Long.parseLong(timestamp);
        } catch (NumberFormatException e) {
            return false;
        }

        long currentTime = System.currentTimeMillis();
        if (timeStampValue < currentTime - TIMESTAMP_VALID_TIME || timeStampValue > currentTime + TIMESTAMP_VALID_TIME) {
            return false;
        }

        return true;
    }
}

usedNonceSet是一个Set类型的变量,用来存储已经使用过的Nonce值。在上面的代码中,在验证请求的Nonce是否合法时,会先判断Nonce是否已经在usedNonceSet中出现过,如果出现过,则认为该请求是重放攻击,返回错误响应。

这个usedNonceSet变量应该是在类的变量里定义并初始化的,并且需要注意的是,这个set要确保线程安全,在不同的线程中能够正确的读写。

private final Set<String> usedNonceSet = Collections.synchronizedSet(new HashSet<>());

这里把usedNonceSet 使用了Collections.synchronizedSet 包装,可以保证线程安全。

需要注意的是,这种方法的限制是有时效性的,在一定时间内重复使用的nonce是不会被拦截的。因此需要定期清空这个set,或者使用其他方式存储已使用过的nonce。

   /**
     * 每分钟执行一次
     */
    @Scheduled(cron = "0 * * * * *")
    public void clearUsedNonceSet() {
        usedNonceSet.clear();
    }

需要注意的是,这些方法都只是简单的清空set,更好的做法是能够记录每个nonce使用的时间,在超过时效性之后删除。

private static final long TIMESTAMP_VALID_TIME = 5 * 60 * 1000;

这里定义了TIMESTAMP_VALID_TIME为5分钟,即请求时间戳的有效时间范围为当前时间前5分钟和当前时间后5分钟。

需要注意的是,这个TIMESTAMP_VALID_TIME是需要根据具体场景和需求来确定的,如果设置过大,则会增加重放攻击的风险,如果设置过小,则会增加误拦截的风险。

以上方法都是定时清空usedNonceSet的方法,可以根据业务需求和资源限制进行选择。

提示:更多内容可以访问Clang’s Blog:https://www.clang.asia

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐