Spring Boot 防重放攻击原理及代码实现

Spring Boot Security About 4,835 words

重放攻击

API重放攻击(Replay Attacks)又称重播攻击、回放攻击,这种攻击会不断恶意或欺诈性地重复一个有效的API请求。攻击者利用网络监听或者其他方式盗取API请求,进行一定的处理后,再把它重新发给认证服务器,是黑客常用的攻击方式之一。

防止重放攻击

使用签名之后,可以对请求的身份进行验证。但不能阻止重放攻击,即攻击者截获请求后,不对请求进行任何调整。直接使用截获的内容重新高频率发送请求。

提供X-Ca-TimestampX-Ca-Noncenonce: number used once)两个可选Http Header,客户端调用API时一起使用这两个参数,可以达到防止重放攻击的目的。

原理

  1. X-Ca-Timestamp:发起请求的时间,可以取自机器的本地实现。当网关收到请求时,会校验这个参数的有效性,误差不超过指定时间(如:15分钟)。
  2. X-Ca-Nonce:这个是请求的唯一标识,一般使用UUID来标识。网关收到这个参数后会校验这个参数的有效性,同样的值,指定时间(如:15分钟)只能被使用一次。
  3. X-Ca-TimestampX-Ca-Nonce和一起辅助参数都加入签名计算,所以请求的任何修改,都会造成签名失败。

实现

Filter 方式(推荐)

@Slf4j
@RequiredArgsConstructor
public class ReplayAttacksFilter extends OncePerRequestFilter {

    private static final String X_CA_Key = "X-Ca-Key";

    private static final String X_CA_TIMESTAMP = "X-Ca-Timestamp";

    private static final String X_CA_NONCE = "X-Ca-Nonce";

    private static final String X_CA_SIGNATURE = "X-Ca-Signature";

    private static final String X_CA_SIGNATURE_METHOD = "X-Ca-Signature-Method";

    private static final String X_CA_SIGNATURE_HEADERS = "X-Ca-Signature-Headers";

    private static final long EXPIRE_MILLIS = 60_000;

    private final Cache cache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String key = request.getHeader(X_CA_Key);
            String timestamp = request.getHeader(X_CA_TIMESTAMP);
            String nonce = request.getHeader(X_CA_NONCE);
            String signature = request.getHeader(X_CA_SIGNATURE);
            String signatureMethod = request.getHeader(X_CA_SIGNATURE_METHOD);
            String signatureHeaders = request.getHeader(X_CA_SIGNATURE_HEADERS);

            if (ObjectUtils.isEmpty(timestamp) || ObjectUtils.isEmpty(nonce) || ObjectUtils.isEmpty(signature)) {
                log.error("Replay Attacks[{}] Invalid Http Headers: [{}, {}, {}], params: {}", request.getRequestURI(), timestamp, nonce, signature, StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8).replaceAll(System.lineSeparator(),""));
                throw new ReplayAttacksException();
            }

            long ts = Long.parseLong(timestamp);
            long currentTs = System.currentTimeMillis();
            long diff = currentTs - ts;
            if (Math.abs(diff) > EXPIRE_MILLIS) {
                log.error("Replay Attacks[{}] timestamp[{}] diff[{}] is out of scope[{}], params: {}", request.getRequestURI(), timestamp, diff, EXPIRE_MILLIS, StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8).replaceAll(System.lineSeparator(),""));
                throw new ReplayAttacksException();
            }

            String text = String.join("", timestamp, nonce);
            String calcSign = DigestUtils.md5DigestAsHex(text.getBytes(StandardCharsets.UTF_8));
            if (!Objects.equals(signature, calcSign)) {
                log.error("Replay Attacks[{}] Invalid Signature[{}], Raw text: {}, Calc Sign: {}, params: {}", request.getRequestURI(), signature, text, calcSign, StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8).replaceAll(System.lineSeparator(),""));
                throw new ReplayAttacksException();
            }

            Object value = cache.get(text);
            if (Objects.nonNull(value)) {
                log.error("Replay Attacks[{}] Exists Nonce[{}], Timestamp is {}", request.getRequestURI(), nonce, value);
                throw new ReplayAttacksException();
            }
            cache.put(Cache.Prefix.REPLAY_ATTACKS + signature, timestamp, EXPIRE_MILLIS, TimeUnit.MILLISECONDS);
            filterChain.doFilter(request, response);
        } catch (ReplayAttacksException | NumberFormatException e) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
        }
    }
}

注册Filter实例。

@Configuration(proxyBeanMethods = false)
public class ReplayAttacksConfig {

    @Bean
    public FilterRegistrationBean<ReplayAttacksFilter> replayAttacksFilter(Cache cache) {
        FilterRegistrationBean<ReplayAttacksFilter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new ReplayAttacksFilter(cache));
        filterRegistrationBean.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE + 1); // after TraceFilter
        filterRegistrationBean.setUrlPatterns(Collections.singleton(Url.Api.FILTER_PATTERN));
        return filterRegistrationBean;
    }

}

Interceptor 方式

Interceptor方式只是与Filter方式的注册方式和作用于不一样,Interceptor作用时间更晚于Filter

InterceptorSpring提供的MVC统一入口DispatcherServletdoDispatch方法中调用applyPreHandle方法被执行。

RequestBodyAdvice 方式

RequestBodyAdvice作用时间更晚于Interceptor

参考

https://help.aliyun.com/document_detail/50041.html

https://help.aliyun.com/document_detail/29475.html

Views: 2,133 · Posted: 2023-01-23

————        END        ————

Give me a Star, Thanks:)

https://github.com/fendoudebb/LiteNote

扫描下方二维码关注公众号和小程序↓↓↓

扫描下方二维码关注公众号和小程序↓↓↓


Today On History
Browsing Refresh