文章目录
- 简述
- 本文涉及代码已开源
- 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 {
/**
* 网关参数配置
*/
@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;
}
}
前端
被防重放拦截
增加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 = 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));
},
请求头信息增加
此时请求头中会增加两个信息,一个是时间戳,一个是签名。后端此时就会验证信息是否合法,合法则放行。