文章目录
- 简述
- 本文涉及代码已开源
- Fir Cloud 完整项目
- 防重放
- 防重放必要性:
- 防重放机制作用:
- 整体效果
- 后端进行处理
- 后端
- 增加防重放开关配置
- 签名密钥 工具类
- 防重放拦截器
- 前端
- 被防重放拦截
- 增加防重放开关配置
- 请求头增加防重放签名处理
- 防重放验证处理函数
- base64加密生成签名
- 请求头信息增加
简述
本文涉及代码已开源
本文网关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
防重放
防重放攻击的必要性主要来自于网络安全中的一个核心原则:确保数据的完整性、机密性和不可否认性。重放攻击是一种常见的安全威胁,它利用网络通信的漏洞来重新发送之前捕获的通信数据,以欺骗系统执行未授权的操作。
防重放必要性:
- 数据完整性:防止攻击者通过重放攻击修改或替换数据,确保数据在传输过程中未被篡改。
- 防止身份盗用:防止攻击者利用截获的认证信息冒充合法用户,从而访问受保护的资源。
- 保护交易:在金融交易和电子商务中,防止攻击者通过重放交易来欺诈。
- 遵守法规:某些行业法规要求实施防重放机制,以符合数据保护和网络安全的法律要求。
- 维护信任:通过保护系统免受重放攻击,维护用户和系统之间的信任关系。
防重放机制作用:
- 验证时间戳:通过在消息中包含时间戳,并确保消息在合理的时间窗口内被接收,可以防止旧消息被重放。
- 数字签名:使用数字签名来验证消息的发送者和内容,确保消息未被篡改,并且发送者的身份得到验证。
- 应用层检查:在应用层进行额外的逻辑检查,例如验证用户的状态或会话,以防止重放攻击。
- 日志和监控:记录和监控网络活动,以便在发生重放攻击时能够快速检测和响应。
通过实施防重放机制,可以显著提高系统的安全性,保护关键数据和操作免受未授权的访问和篡改。在设计系统时,应考虑潜在的重放攻击,并采取适当的措施来防范。
整体效果
前端如果没有进行防重放信息处理,则会被直接拦截。
后端进行处理
后端
增加防重放开关配置
# 防重放replay: true
/*** 防重放攻击*/private boolean replay;
签名密钥 工具类
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 {/*** 网关参数配置*/@Resourceprivate 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();}@Overridepublic 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和TimestampString 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);}@Overridepublic 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;}
}
前端
被防重放拦截
增加nonce(签名),t(时间戳)。
增加防重放开关配置
// 防重放
const replay = true;
请求头增加防重放签名处理
gatewayRequest中增加
// 防重放请求通过信息if (replay) {this.replayAttack(request);}
防重放验证处理函数
//************************************防重放-start/*** 防重放验证** @param config Axios请求的配置对象*/replayAttack(config) {let params = config.params;let reqData = config.params;let t = new Date().getTime();if (t) {config.headers.t = tif (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));},
请求头信息增加
此时请求头中会增加两个信息,一个是时间戳,一个是签名。后端此时就会验证信息是否合法,合法则放行。