简述

本文涉及代码已开源

本文网关gateway,微服务,vue已开源到gitee
杉极简 / gateway网关阶段学习
https://gitee.com/dong-puen/gateway-stages

Fir Cloud 完整项目

该内容完整项目如下
Fir Cloud v1.0.0
https://gitee.com/dong-puen/fir-cloud
https://github.com/firLucky/fir-cloud
在这里插入图片描述

防重放

防重放攻击的必要性主要来自于网络安全中的一个核心原则:确保数据的完整性、机密性和不可否认性。重放攻击是一种常见的安全威胁,它利用网络通信的漏洞来重新发送之前捕获的通信数据,以欺骗系统执行未授权的操作。

防重放必要性:

  1. 数据完整性:防止攻击者通过重放攻击修改或替换数据,确保数据在传输过程中未被篡改。
  2. 防止身份盗用:防止攻击者利用截获的认证信息冒充合法用户,从而访问受保护的资源。
  3. 保护交易:在金融交易和电子商务中,防止攻击者通过重放交易来欺诈。
  4. 遵守法规:某些行业法规要求实施防重放机制,以符合数据保护和网络安全的法律要求。
  5. 维护信任:通过保护系统免受重放攻击,维护用户和系统之间的信任关系。

防重放机制作用:

  1. 验证时间戳:通过在消息中包含时间戳,并确保消息在合理的时间窗口内被接收,可以防止旧消息被重放。
  2. 数字签名:使用数字签名来验证消息的发送者和内容,确保消息未被篡改,并且发送者的身份得到验证。
  3. 应用层检查:在应用层进行额外的逻辑检查,例如验证用户的状态或会话,以防止重放攻击。
  4. 日志和监控:记录和监控网络活动,以便在发生重放攻击时能够快速检测和响应。

通过实施防重放机制,可以显著提高系统的安全性,保护关键数据和操作免受未授权的访问和篡改。在设计系统时,应考虑潜在的重放攻击,并采取适当的措施来防范。

整体效果

前端如果没有进行防重放信息处理,则会被直接拦截。

后端进行处理

image.png

后端

增加防重放开关配置

  # 防重放
  replay: true

    /**
     * 防重放攻击
     */
    private boolean replay;

image.png
image.png

签名密钥 工具类

package com.fir.gateway.utils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * 签名密钥 工具类
 * 
 * @author fir
 */
public class SignatureUtils {

    /**
     * 解密前端防重放校验的签名密钥(失败未验证)
     *
     * @param signature 签名密钥
     * @param secretKey 加密密钥
     * @return 返回解密结果
     */
    public static String decryptSignature(String signature, String secretKey) {
        try {
            // 使用HMAC-SHA256算法
            Mac sha256Hmac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            sha256Hmac.init(secretKeySpec);

            // 对签名进行Base64解码
            byte[] signatureBytes = Base64.getDecoder().decode(signature);

            // 进行解密操作
            byte[] decryptedBytes = sha256Hmac.doFinal(signatureBytes);

            // 将解密结果转为字符串
            String decryptedSignature = new String(decryptedBytes, StandardCharsets.UTF_8);

            return decryptedSignature;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 解密前端防重放校验的签名密钥
     *
     * @param signature 签名密钥
     * @return 返回解密结果
     */
    public static String decryptSignatureBase64(String signature) {
        try {
            // 对签名进行Base64解码
            byte[] signatureBytes = Base64.getDecoder().decode(signature);

            // 将解密结果转为字符串

            return new String(signatureBytes, StandardCharsets.UTF_8);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

防重放拦截器

package com.fir.gateway.filter.request;

import com.alibaba.fastjson.JSONObject;
import com.fir.gateway.config.GlobalConfig;
import com.fir.gateway.config.result.AjaxResult;
import com.fir.gateway.config.result.AjaxStatus;
import com.fir.gateway.dto.ReplayAttackInfo;
import com.fir.gateway.utils.SignatureUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
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 javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.*;


/**
 * 防重放攻击-请求拦截器
 *
 * @author fir
 */
@Component
@Slf4j
public class ReplayAttackFilter implements GlobalFilter, Ordered {


    /**
     * 网关参数配置
     */
    @Resource
    private GlobalConfig globalConfig;


    /**
     * 5 * 60 * 1000 表示5分钟的间隔,用于防重放的间隔之中
     */
    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) {
        log.info("防重放攻击验证:start");
        boolean replay = globalConfig.isReplay();
        if(replay) {
            // 白名单路由判断
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getPath().toString();
            List<String> whiteUrls = globalConfig.getWhiteUrls();
            if(whiteUrls.contains(path)){
                log.info("防重放攻击验证:true,白名单");
                return chain.filter(exchange);
            }

            // 从请求头中获取Nonce和Timestamp
            String nonce = exchange.getRequest().getHeaders().getFirst("n");
            String timestamp = exchange.getRequest().getHeaders().getFirst("t");
            String s = exchange.getRequest().getHeaders().getFirst("s");
            // 验证Nonce和Timestamp是否合法
            boolean validateKey = validateNonceAndTimestamp(nonce, timestamp, s);
            if (validateKey) {
                // 如果合法,则放行请求
                log.info("防重放攻击验证:true");
            } else {
                log.info("防重放攻击验证:false");
                // 如果不合法,则返回错误响应
                ServerHttpResponse response = exchange.getResponse();
                // 自定义返回体描述
                AjaxResult error = AjaxResult.error(AjaxStatus.ANTI_REPLAY_VERIFY_FAILED);
                String resData = JSONObject.toJSONString(error);

                byte[] responseBody = resData.getBytes(StandardCharsets.UTF_8);

                response.getHeaders().setContentLength(responseBody.length);
                response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

                return response.writeWith(Mono.just(response.bufferFactory().wrap(responseBody)));

            }
        }else {
            log.info("防重放攻击验证:true,验证已关闭");
        }

        return chain.filter(exchange);
    }


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


    /**
     * 根据请求时间戳,与请求签名密钥,判断请求是否是合法的
     *
     * @param nonce 请求签名密钥
     * @param timestamp 请求时间戳
     * @return 是否合法
     */
    private boolean validateNonceAndTimestamp(String nonce, String timestamp, String s) {
        // 判断Nonce和Timestamp是否为空
        if (nonce == null || timestamp == null) {
            log.error("防重放攻击验证:非法请求,无请求时间戳");
            return false;
        }

        // 验证Nonce是否已经使用过
        if (usedNonceSet.contains(nonce)) {
            log.error("防重放攻击验证:请求签名已使用");
            return false;
        } else {
            // 将本次的请求签名记录,用于下次判断是否有相同的请求签名
            usedNonceSet.add(nonce);
        }

        // 判断事件戳与数据签名是否相同
        String str = SignatureUtils.decryptSignatureBase64(nonce);
        ReplayAttackInfo replayAttackInfo = JSONObject.parseObject(str, ReplayAttackInfo.class);
        String t = replayAttackInfo != null ? replayAttackInfo.getT() : null;
        if (StringUtils.isBlank(t) || !timestamp.equals(t)){
            log.error("防重放攻击验证:非法请求,请求时间非法");
            return false;
        }

        // 验证Timestamp是否在合理时间范围内
        long timeStampValue;
        try {
            timeStampValue = Long.parseLong(timestamp);
        } catch (NumberFormatException e) {
            log.error("防重放攻击验证:非法请求,请求时间错误");
            return false;
        }

        long currentTime = System.currentTimeMillis();

        // 判断请求是是否是在n分钟之前请求的
        boolean a = timeStampValue >= currentTime - TIMESTAMP_VALID_TIME;
        // 判断请求是是否是在n分钟后前请求的
        boolean b = timeStampValue <= currentTime + TIMESTAMP_VALID_TIME;

        boolean c = a && b;
        if (!c){
            log.info("防重放攻击验证:请求过期");
        }
        return c;
    }
}

前端

被防重放拦截

image.png
增加nonce(签名),t(时间戳)。

增加防重放开关配置

// 防重放
const replay = true;

image.png

请求头增加防重放签名处理

gatewayRequest中增加

            // 防重放请求通过信息
            if (replay) {
                this.replayAttack(request);
            }

image.png

防重放验证处理函数

    //************************************防重放-start
    /**
     * 防重放验证
     *
     * @param config Axios请求的配置对象
     */
    replayAttack(config) {
        let params = config.params;
        let reqData = config.params;
        let t = new Date().getTime();
        if (t) {
            config.headers.t = t

            if (reqData == null) {
                params = {
                    't': t,
                }
            } else {
                // params = JSON.parse(reqData)
                params["t"] = t;
            }

            const data = JSON.stringify(params);
            config.headers.n = this.gBase(data);
        }
    },
    //************************************防重放-end

base64加密生成签名

    /**
     * base64加密
     * @param data data 待加密数据
     */
    gBase(data) {
        return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data));
    },

请求头信息增加

此时请求头中会增加两个信息,一个是时间戳,一个是签名。后端此时就会验证信息是否合法,合法则放行。
image.png

Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐