当前位置:   article > 正文

接口防篡改+防重放攻击

接口防篡改+防重放攻击

接口防止重放攻击:重放攻击是指攻击者截获了一次有效请求(如交易请求),并在之后的时间里多次发送相同的请求,从而达到欺骗系统的目的。为了防止重放攻击,通常需要在系统中引入一种机制,使得每个请求都有一个唯一的标识符(如时间戳、序列号等),并在处理请求时检查这个标识符,确保请求没有被重复处理。
接口幂等性:幂等性是指一个操作具有不变性,即多次执行该操作会产生相同的结果。例如,支付金额、提交表单等操作都具有幂等性。为了保证系统的幂等性,需要确保在处理请求时,对于相同的输入参数,系统只执行一次相应的操作,而不会对同一个输入参数进行多次处理。这可以通过在数据库中设置唯一约束、使用乐观锁等方式实现。

方案:

接口防止重放攻击:基于nonce + timestamp 的方案
nonce的意思是仅一次有效的随机字符串,要求每次请求时该参数要保证不同。实际使用用户信息+时间戳+随机数等信息做个哈希之后,作为nonce参数。

此时服务端的处理流程如下:

        去 redis 中查找是否有 key 为 nonce:{nonce}的 string

        如果没有,则创建这个 key,把这个 key 失效的时间和验证 timestamp 失效的时间一致,比如是 60s。

        如果有,说明这个 key 在 60s 内已经被使用了,那么这个请求就可以判断为重放请求。

接口安全设计之防篡改和防重放_接口防篡改机制-CSDN博客

如何保证接口安全,做到防篡改防重放?_接口防止串改-CSDN博客(下面的代码参加该博客)

防篡改

我们知道http 是一种无状态的协议,服务端并不知道客户端发送的请求是否合法,也并不知道请求中的参数是否正确。

举个例子, 现在有个充值的接口,调用后可以给用户增加对应的余额。

http://localhost/api/user/recharge?user_id=1001&amount=10

如果非法用户通过抓包获取到接口参数后,修改user_id 或 amount的值就可以实现给任意账户添加余额的目的。

如何解决

采用https协议可以将传输的明文进行加密,但是黑客仍然可以截获传输的数据包,进一步伪造请求进行重放攻击。如果黑客使用特殊手段让请求方设备使用了伪造的证书进行通信,那么https加密的内容也会被解密。

一般的做法有2种:

采用https方式把接口的数据进行加密传输,即便是被黑客破解,黑客也花费大量的时间和精力去破解。

接口后台对接口的请求参数进行验证,防止被黑客篡改;

步骤1:客户端使用约定好的秘钥对传输的参数进行加密,得到签名值sign1,并且将签名值也放入请求的参数中,发送请求给服务端

步骤2:服务端接收到客户端的请求,然后使用约定好的秘钥对请求的参数再次进行签名,得到签名值sign2。

步骤3:服务端比对sign1和sign2的值,如果不一致,就认定为被篡改,非法请求。

防重放

防重放也叫防复用,简单来说就是我获取到这个请求的信息之后什么也不改,,直接拿着接口的参数 重复请求这个充值的接口。此时我的请求是合法的,
因为所有参数都是跟合法请求一模一样的。

重放攻击会造成两种后果:

  1. 针对插入数据库接口:重放攻击,会出现大量重复数据,甚至垃圾数据会把数据库撑爆。

  2. 针对查询的接口:黑客一般是重点攻击慢查询接口,例如一个慢查询接口1s,只要黑客发起重放攻击,就必然造成系统被拖垮,数据库查询被阻塞死。

对于重放攻击一般有两种做法:

基于timestamp的方案

每次HTTP请求,都需要加上timestamp参数,然后把timestamp和其他参数一起进行数字签名。因为一次正常的HTTP请求,从发出到达服务器一般都不会超过60s,所以服务器收到HTTP请求之后,首先判断时间戳参数与当前时间比较,是否超过了60s,如果超过了则认为是非法请求。

一般情况下,黑客从抓包重放请求耗时远远超过了60s,所以此时请求中的timestamp参数已经失效了。如果黑客修改timestamp参数为当前的时间戳,则sign1参数对应的数字签名就会失效,因为黑客不知道签名秘钥,没有办法生成新的数字签名。

但是这种方式的漏洞也是显而易见,如果在60s之内进行重放攻击,那就没办法了,所以这种方式不能保证请求仅一次有效。

老鸟们一般会采取下面这种方案,既可以解决接口重放问题,又可以解决接口一次请求有效的问题。

基于nonce + timestamp 的方案

nonce的意思是仅一次有效的随机字符串,要求每次请求时该参数要保证不同。实际使用用户信息+时间戳+随机数等信息做个哈希之后,作为nonce参数。

此时服务端的处理流程如下:

  1. 去 redis 中查找是否有 key 为 nonce:{nonce}的 string

  2. 如果没有,则创建这个 key,把这个 key 失效的时间和验证 timestamp 失效的时间一致,比如是 60s。

  3. 如果有,说明这个 key 在 60s 内已经被使用了,那么这个请求就可以判断为重放请求。

这种方案nonce和timestamp参数都作为签名的一部分传到后端,基于timestamp方案可以让黑客只能在60s内进行重放攻击,加上nonce随机数以后可以保证接口只能被调用一次,可以很好的解决重放攻击问题。

放篡改+防重放实现代码实现

接下来通过实际代码来看看如何实现接口的防篡改和防重放。

1、构建请求头对象
  1. @Data
  2. @Builder
  3. public class RequestHeader {
  4.    private String sign ;
  5.    private Long timestamp ;
  6.    private String nonce;
  7. }
2、工具类从HttpServletRequest获取请求参数
  1. @Slf4j
  2. @UtilityClass
  3. public class HttpDataUtil {
  4.     /**
  5.      * post请求处理:获取 Body 参数,转换为SortedMap
  6.      *
  7.      * @param request
  8.      */
  9.     public  SortedMap<StringString> getBodyParams(final HttpServletRequest request) throws IOException {
  10.         byte[] requestBody = StreamUtils.copyToByteArray(request.getInputStream());
  11.         String body = new String(requestBody);
  12.         return JsonUtil.json2Object(body, SortedMap.class);
  13.     }
  14.     /**
  15.      * get请求处理:将URL请求参数转换成SortedMap
  16.      */
  17.     public static SortedMap<StringString> getUrlParams(HttpServletRequest request) {
  18.         String param = "";
  19.         SortedMap<StringString> result = new TreeMap<>();
  20.         if (StringUtils.isEmpty(request.getQueryString())) {
  21.             return result;
  22.         }
  23.         try {
  24.             param = URLDecoder.decode(request.getQueryString(), "utf-8");
  25.         } catch (UnsupportedEncodingException e) {
  26.             e.printStackTrace();
  27.         }
  28.         String[] params = param.split("&");
  29.         for (String s : params) {
  30.             String[] array=s.split("=");
  31.             result.put(array[0], array[1]);
  32.         }
  33.         return result;
  34.     }
  35. }

这里的参数放入SortedMap中对其进行字典排序,前端构建签名时同样需要对参数进行字典排序。

3、签名验证工具类
  1. @Slf4j
  2. @UtilityClass
  3. public class SignUtil {
  4.     /**
  5.      * 验证签名
  6.      * 验证算法:把timestamp + JsonUtil.object2Json(SortedMap)合成字符串,然后MD5
  7.      */
  8.     @SneakyThrows
  9.     public  boolean verifySign(SortedMap<StringString> map, RequestHeader requestHeader) {
  10.         String params = requestHeader.getNonce() + requestHeader.getTimestamp() + JsonUtil.object2Json(map);
  11.         return verifySign(params, requestHeader);
  12.     }
  13.     /**
  14.      * 验证签名
  15.      */
  16.     public boolean verifySign(String params, RequestHeader requestHeader) {
  17.         log.debug("客户端签名: {}", requestHeader.getSign());
  18.         if (StringUtils.isEmpty(params)) {
  19.             return false;
  20.         }
  21.         log.info("客户端上传内容: {}", params);
  22.         String paramsSign = DigestUtils.md5DigestAsHex(params.getBytes()).toUpperCase();
  23.         log.info("客户端上传内容加密后的签名结果: {}", paramsSign);
  24.         return requestHeader.getSign().equals(paramsSign);
  25.     }
  26. }
4、HttpServletRequest包装类
  1. public class SignRequestWrapper extends HttpServletRequestWrapper {
  2.     //用于将流保存下来
  3.     private byte[] requestBody = null;
  4.     public SignRequestWrapper(HttpServletRequest request) throws IOException {
  5.         super(request);
  6.         requestBody = StreamUtils.copyToByteArray(request.getInputStream());
  7.     }
  8.     @Override
  9.     public ServletInputStream getInputStream() throws IOException {
  10.         final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
  11.         return new ServletInputStream() {
  12.             @Override
  13.             public boolean isFinished() {
  14.                 return false;
  15.             }
  16.             @Override
  17.             public boolean isReady() {
  18.                 return false;
  19.             }
  20.             @Override
  21.             public void setReadListener(ReadListener readListener) {
  22.             }
  23.             @Override
  24.             public int read() throws IOException {
  25.                 return bais.read();
  26.             }
  27.         };
  28.     }
  29.     @Override
  30.     public BufferedReader getReader() throws IOException {
  31.         return new BufferedReader(new InputStreamReader(getInputStream()));
  32.     }
  33. }

防篡改和防重放我们会通过SpringBoot
Filter来实现,而编写的filter过滤器需要读取request数据流,但是request数据流只能读取一次,需要自己实现HttpServletRequestWrapper对数据流包装,目的是将request流保存下来。

5、创建过滤器实现安全校验
  1. @Configuration
  2. public class SignFilterConfiguration {
  3.     @Value("${sign.maxTime}")
  4.     private String signMaxTime;
  5.     //filter中的初始化参数
  6.     private Map<StringString> initParametersMap =  new HashMap<>();
  7.     @Bean
  8.     public FilterRegistrationBean contextFilterRegistrationBean() {
  9.         initParametersMap.put("signMaxTime",signMaxTime);
  10.         FilterRegistrationBean registration = new FilterRegistrationBean();
  11.         registration.setFilter(signFilter());
  12.         registration.setInitParameters(initParametersMap);
  13.         registration.addUrlPatterns("/sign/*");
  14.         registration.setName("SignFilter");
  15.         // 设置过滤器被调用的顺序
  16.         registration.setOrder(1);
  17.         return registration;
  18.     }
  19.     @Bean
  20.     public Filter signFilter() {
  21.         return new SignFilter();
  22.     }
  23. }
  24. @Slf4j
  25. public class SignFilter implements Filter {
  26.     @Resource
  27.     private RedisUtil redisUtil;
  28.     //从fitler配置中获取sign过期时间
  29.     private Long signMaxTime;
  30.     private static final String NONCE_KEY = "x-nonce-";
  31.     @Override
  32.     public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  33.         HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
  34.         HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
  35.         log.info("过滤URL:{}", httpRequest.getRequestURI());
  36.         HttpServletRequestWrapper requestWrapper = new SignRequestWrapper(httpRequest);
  37.         //构建请求头
  38.         RequestHeader requestHeader = RequestHeader.builder()
  39.                 .nonce(httpRequest.getHeader("x-Nonce"))
  40.                 .timestamp(Long.parseLong(httpRequest.getHeader("X-Time")))
  41.                 .sign(httpRequest.getHeader("X-Sign"))
  42.                 .build();
  43.         //验证请求头是否存在
  44.         if(StringUtils.isEmpty(requestHeader.getSign()) || ObjectUtils.isEmpty(requestHeader.getTimestamp()) || StringUtils.isEmpty(requestHeader.getNonce())){
  45.             responseFail(httpResponse, ReturnCode.ILLEGAL_HEADER);
  46.             return;
  47.         }
  48.         /*
  49.          * 1.重放验证
  50.          * 判断timestamp时间戳与当前时间是否操过60s(过期时间根据业务情况设置),如果超过了就提示签名过期。
  51.          */
  52.         long now = System.currentTimeMillis() / 1000;
  53.         if (now - requestHeader.getTimestamp() > signMaxTime) {
  54.             responseFail(httpResponse,ReturnCode.REPLAY_ERROR);
  55.             return;
  56.         }
  57.         //2. 判断nonce
  58.         boolean nonceExists = redisUtil.hasKey(NONCE_KEY + requestHeader.getNonce());
  59.         if(nonceExists){
  60.             //请求重复
  61.             responseFail(httpResponse,ReturnCode.REPLAY_ERROR);
  62.             return;
  63.         }else {
  64.             redisUtil.set(NONCE_KEY+requestHeader.getNonce(), requestHeader.getNonce(), signMaxTime);
  65.         }
  66.         boolean accept;
  67.         SortedMap<StringString> paramMap;
  68.         switch (httpRequest.getMethod()){
  69.             case "GET":
  70.                 paramMap = HttpDataUtil.getUrlParams(requestWrapper);
  71.                 accept = SignUtil.verifySign(paramMap, requestHeader);
  72.                 break;
  73.             case "POST":
  74.                 paramMap = HttpDataUtil.getBodyParams(requestWrapper);
  75.                 accept = SignUtil.verifySign(paramMap, requestHeader);
  76.                 break;
  77.             default:
  78.                 accept = true;
  79.                 break;
  80.         }
  81.         if (accept) {
  82.             filterChain.doFilter(requestWrapper, servletResponse);
  83.         } else {
  84.             responseFail(httpResponse,ReturnCode.ARGUMENT_ERROR);
  85.             return;
  86.         }
  87.     }
  88.     private void responseFail(HttpServletResponse httpResponse, ReturnCode returnCode)  {
  89.         ResultData<Object> resultData = ResultData.fail(returnCode.getCode(), returnCode.getMessage());
  90.         WebUtils.writeJson(httpResponse,resultData);
  91.     }
  92.     @Override
  93.     public void init(FilterConfig filterConfig) throws ServletException {
  94.         String signTime = filterConfig.getInitParameter("signMaxTime");
  95.         signMaxTime = Long.parseLong(signTime);
  96.     }
  97. }
6、Redis工具类
  1. @Component
  2. public class RedisUtil {
  3.     @Resource
  4.     private RedisTemplate<StringObject> redisTemplate;
  5.     /**
  6.      * 判断key是否存在
  7.      * @param key 键
  8.      * @return true 存在 false不存在
  9.      */
  10.     public boolean hasKey(String key) {
  11.         try {
  12.             return Boolean.TRUE.equals(redisTemplate.hasKey(key));
  13.         } catch (Exception e) {
  14.             e.printStackTrace();
  15.             return false;
  16.         }
  17.     }
  18.     /**
  19.      * 普通缓存放入并设置时间
  20.      * @param key   键
  21.      * @param value 值
  22.      * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
  23.      * @return true成功 false 失败
  24.      */
  25.     public boolean set(String keyObject value, long time) {
  26.         try {
  27.             if (time > 0) {
  28.                 redisTemplate.opsForValue().set(keyvaluetime, TimeUnit.SECONDS);
  29.             } else {
  30.                 set(keyvalue);
  31.             }
  32.             return true;
  33.         } catch (Exception e) {
  34.             e.printStackTrace();
  35.             return false;
  36.         }
  37.     }
  38.     /**
  39.      * 普通缓存放入
  40.      * @param key   键
  41.      * @param value 值
  42.      * @return true成功 false失败
  43.      */
  44.     public boolean set(String keyObject value) {
  45.         try {
  46.             redisTemplate.opsForValue().set(keyvalue);
  47.             return true;
  48.         } catch (Exception e) {
  49.             e.printStackTrace();
  50.             return false;
  51.         }
  52.     }
  53. }
项目源码地址

https://github.com/jianzh5/cloud-blog/tree/main/cloud-demo

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/773719
推荐阅读
相关标签
  

闽ICP备14008679号