Spring Cloud Gateway网关XSS过滤方式

作者:千年的心 时间:2021-08-07 13:16:53 

XSS是一种经常出现在web应用中的计算机安全漏洞,具体信息请自行Google。本文只分享在Spring Cloud Gateway中执行通用的XSS防范。首次作文,全是代码,若有遗漏不明之处,请各位看官原谅指点。

使用版本

  • Spring Cloud版本为 Greenwich.SR4

  • Spring Boot版本为 2.1.11.RELEASE

1.创建一个Filter

特别注意的是在处理完成之后需要重新构造请求,否则后续业务无法获得参数。


import io.netty.buffer.ByteBufAllocator;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.DigestUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.validation.constraints.NotEmpty;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
/**
* XSS过滤
*
* @author lieber
*/
@Component
@Slf4j
@ConfigurationProperties("config.xss")
@Data
public class XssFilter implements GlobalFilter, Ordered {
   private List<XssWhiteUrl> whiteUrls;
   @Override
   public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
       ServerHttpRequest request = exchange.getRequest();
       URI uri = request.getURI();
       String method = request.getMethodValue();
       // 判断是否在白名单中
       if (this.white(uri.getPath(), method)) {
           return chain.filter(exchange);
       }
       // 只拦截POST和PUT请求
       if ((HttpMethod.POST.name().equals(method) || HttpMethod.PUT.name().equals(method))) {
           return DataBufferUtils.join(request.getBody())
                   .flatMap(dataBuffer -> {
                       // 取出body中的参数
                       byte[] oldBytes = new byte[dataBuffer.readableByteCount()];
                       dataBuffer.read(oldBytes);
                       String bodyString = new String(oldBytes, StandardCharsets.UTF_8);
                       log.debug("原请求参数为:{}", bodyString);
                       // 执行XSS清理
                       bodyString = XssUtil.INSTANCE.cleanXss(bodyString);
                       log.debug("修改后参数为:{}", bodyString);
                       ServerHttpRequest newRequest = request.mutate().uri(uri).build();
                       // 重新构造body
                       byte[] newBytes = bodyString.getBytes(StandardCharsets.UTF_8);
                       DataBuffer bodyDataBuffer = toDataBuffer(newBytes);
                       Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
                       // 重新构造header
                       HttpHeaders headers = new HttpHeaders();
                       headers.putAll(request.getHeaders());
                       // 由于修改了传递参数,需要重新设置CONTENT_LENGTH,长度是字节长度,不是字符串长度
                       int length = newBytes.length;
                       headers.remove(HttpHeaders.CONTENT_LENGTH);
                       headers.setContentLength(length);
                       headers.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=utf8");
                       // 重写ServerHttpRequestDecorator,修改了body和header,重写getBody和getHeaders方法
                       newRequest = new ServerHttpRequestDecorator(newRequest) {
                           @Override
                           public Flux<DataBuffer> getBody() {
                               return bodyFlux;
                           }
                           @Override
                           public HttpHeaders getHeaders() {
                               return headers;
                           }
                       };
                       return chain.filter(exchange.mutate().request(newRequest).build());
                   });
       } else {
           return chain.filter(exchange);
       }
   }
   /**
    * 是否是白名单
    *
    * @param url    路由
    * @param method 请求方式
    * @return true/false
    */
   private boolean white(String url, String method) {
       return whiteUrls != null && whiteUrls.contains(XssWhiteUrl.builder().url(url).method(method).build());
   }
   /**
    * 字节数组转DataBuffer
    *
    * @param bytes 字节数组
    * @return DataBuffer
    */
   private DataBuffer toDataBuffer(byte[] bytes) {
       NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
       DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
       buffer.write(bytes);
       return buffer;
   }
   public static final int ORDER = 10;
   @Override
   public int getOrder() {
       return ORDER;
   }
   @Data
   @Validated
   @AllArgsConstructor
   @NoArgsConstructor
   private static class XssWhiteUrl {
       @NotEmpty
       private String url;
       @NotEmpty
       private String method;
   }
}

2. 处理XSS字符串

这里大范围采用Jsoup处理,然后根据自己的业务做了一部分定制。较为特殊的是,我们将字符串中含有'</'标示为这段文本是富文本。在清除xss攻击字符串方法时优化空间较大。


import com.alibaba.fastjson.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Whitelist;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* xss拦截工具类
*
* @author lieber
*/
public enum XssUtil {
   /**
    * 实例
    */
   INSTANCE;
   private final static String RICH_TEXT = "</";
   /**
    * 自定义白名单
    */
   private final static Whitelist CUSTOM_WHITELIST = Whitelist.relaxed()
           .addAttributes("video", "width", "height", "controls", "alt", "src")
           .addAttributes(":all", "style", "class");
   /**
    * jsoup不格式化代码
    */
   private final static Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);
   /**
    * 清除json对象中的xss攻击字符
    *
    * @param val json对象字符串
    * @return 清除后的json对象字符串
    */
   private String cleanObj(String val) {
       JSONObject jsonObject = JSONObject.parseObject(val);
       for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
           if (entry.getValue() != null && entry.getValue() instanceof String) {
               String str = (String) entry.getValue();
               str = this.cleanXss(str);
               entry.setValue(str);
           }
       }
       return jsonObject.toJSONString();
   }
   /**
    * 清除json数组中的xss攻击字符
    *
    * @param val json数组字符串
    * @return 清除后的json数组字符串
    */
   private String cleanArr(String val) {
       List<String> list = JSONObject.parseArray(val, String.class);
       List<String> result = new ArrayList<>(list.size());
       for (String str : list) {
           str = this.cleanXss(str);
           result.add(str);
       }
       return JSONObject.toJSONString(result);
   }
   /**
    * 清除xss攻击字符串,此处优化空间较大
    *
    * @param str 字符串
    * @return 清除后无害的字符串
    */
   public String cleanXss(String str) {
       if (JsonUtil.INSTANCE.isJsonObj(str)) {
           str = this.cleanObj(str);
       } else if (JsonUtil.INSTANCE.isJsonArr(str)) {
           str = this.cleanArr(str);
       } else {
           boolean richText = this.richText(str);
           if (!richText) {
               str = str.trim();
               str = str.replaceAll(" +", " ");
           }
           String afterClean = Jsoup.clean(str, "", CUSTOM_WHITELIST, OUTPUT_SETTINGS);
           if (paramError(richText, afterClean, str)) {
               throw new BizRunTimeException(ApiCode.PARAM_ERROR, "参数包含特殊字符");
           }
           str = richText ? afterClean : this.backSpecialStr(afterClean);
       }
       return str;
   }
   /**
    * 判断是否是富文本
    *
    * @param str 待判断字符串
    * @return true/false
    */
   private boolean richText(String str) {
       return str.contains(RICH_TEXT);
   }
   /**
    * 判断是否参数错误
    *
    * @param richText   是否富文本
    * @param afterClean 清理后字符
    * @param str        原字符串
    * @return true/false
    */
   private boolean paramError(boolean richText, String afterClean, String str) {
       // 如果包含富文本字符,那么不是参数错误
       if (richText) {
           return false;
       }
       // 如果清理后的字符和清理前的字符匹配,那么不是参数错误
       if (Objects.equals(str, afterClean)) {
           return false;
       }
       // 如果仅仅包含可以通过的特殊字符,那么不是参数错误
       if (Objects.equals(str, this.backSpecialStr(afterClean))) {
           return false;
       }
       // 如果还有......
       return true;
   }
   /**
    * 转义回特殊字符
    *
    * @param str 已经通过转义字符
    * @return 转义后特殊字符
    */
   private String backSpecialStr(String str) {
       return str.replaceAll("&amp;", "&");
   }
}

3.其它使用到的工具


import com.alibaba.fastjson.JSONObject;
import org.springframework.util.StringUtils;
/**
* JSON处理工具类
*
* @author lieber
*/
public enum JsonUtil {
   /**
    * 实例
    */
   INSTANCE;
   /**
    * json对象字符串开始标记
    */
   private final static String JSON_OBJECT_START = "{";
   /**
    * json对象字符串结束标记
    */
   private final static String JSON_OBJECT_END = "}";
   /**
    * json数组字符串开始标记
    */
   private final static String JSON_ARRAY_START = "[";
   /**
    * json数组字符串结束标记
    */
   private final static String JSON_ARRAY_END = "]";
   /**
    * 判断字符串是否json对象字符串
    *
    * @param val 字符串
    * @return true/false
    */
   public boolean isJsonObj(String val) {
       if (StringUtils.isEmpty(val)) {
           return false;
       }
       val = val.trim();
       if (val.startsWith(JSON_OBJECT_START) && val.endsWith(JSON_OBJECT_END)) {
           try {
               JSONObject.parseObject(val);
               return true;
           } catch (Exception e) {
               return false;
           }
       }
       return false;
   }
   /**
    * 判断字符串是否json数组字符串
    *
    * @param val 字符串
    * @return true/false
    */
   public boolean isJsonArr(String val) {
       if (StringUtils.isEmpty(val)) {
           return false;
       }
       val = val.trim();
       if (StringUtils.isEmpty(val)) {
           return false;
       }
       val = val.trim();
       if (val.startsWith(JSON_ARRAY_START) && val.endsWith(JSON_ARRAY_END)) {
           try {
               JSONObject.parseArray(val);
               return true;
           } catch (Exception e) {
               return false;
           }
       }
       return false;
   }
   /**
    * 判断对象是否是json对象
    *
    * @param obj 待判断对象
    * @return true/false
    */
   public boolean isJsonObj(Object obj) {
       String str = JSONObject.toJSONString(obj);
       return this.isJsonObj(str);
   }
   /**
    * 判断字符串是否json字符串
    *
    * @param str 字符串
    * @return true/false
    */
   public boolean isJson(String str) {
       if (StringUtils.isEmpty(str)) {
           return false;
       }
       return this.isJsonObj(str) || this.isJsonArr(str);
   }
}

大功告成。

----------------手动分隔----------------

修改

感谢@chang_p_x的指正,在第一步创建Filter时有问题,原因是使用了新旧代码的问题,现已经将元代码放在正文,新代码如下


@Override
   public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
       ServerHttpRequest request = exchange.getRequest();
       URI uri = request.getURI();
       String method = request.getMethodValue();
       if (this.white(uri.getPath(), method)) {
           return chain.filter(exchange);
       }
       if ((HttpMethod.POST.name().equals(method) || HttpMethod.PUT.name().equals(method))) {
           return DataBufferUtils.join(request.getBody()).flatMap(d -> Mono.just(Optional.of(d))).defaultIfEmpty(Optional.empty())
                   .flatMap(optional -> {
                       // 取出body中的参数
                       String bodyString = "";
                       if (optional.isPresent()) {
                           byte[] oldBytes = new byte[optional.get().readableByteCount()];
                           optional.get().read(oldBytes);
                           bodyString = new String(oldBytes, StandardCharsets.UTF_8);
                       }
                       HttpHeaders httpHeaders = request.getHeaders();
                       // 执行XSS清理
                       log.debug("{} - [{}:{}] XSS处理前参数:{}", method, uri.getPath(), bodyString);
                       bodyString = XssUtil.INSTANCE.cleanXss(bodyString);
                       log.info("{} - [{}:{}] 参数:{}", method, uri.getPath(), bodyString);

ServerHttpRequest newRequest = request.mutate().uri(uri).build();
                       // 重新构造body
                       byte[] newBytes = bodyString.getBytes(StandardCharsets.UTF_8);
                       DataBuffer bodyDataBuffer = toDataBuffer(newBytes);
                       Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
                       // 重新构造header
                       HttpHeaders headers = new HttpHeaders();
                       headers.putAll(httpHeaders);
                       // 由于修改了传递参数,需要重新设置CONTENT_LENGTH,长度是字节长度,不是字符串长度
                       int length = newBytes.length;
                       headers.remove(HttpHeaders.CONTENT_LENGTH);
                       headers.setContentLength(length);
                       headers.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=utf8");
                       // 重写ServerHttpRequestDecorator,修改了body和header,重写getBody和getHeaders方法
                       newRequest = new ServerHttpRequestDecorator(newRequest) {
                           @Override
                           public Flux<DataBuffer> getBody() {
                               return bodyFlux;
                           }
                           @Override
                           public HttpHeaders getHeaders() {
                               return headers;
                           }
                       };
                       return chain.filter(exchange.mutate().request(newRequest).build());
                   });
       } else {
           return chain.filter(exchange);
       }
   }

来源:https://blog.csdn.net/u010044936/article/details/107067938

标签:SpringCloud,Gateway,网关,过滤
0
投稿

猜你喜欢

  • 解析Java中如何获取Spring中配置的bean

    2023-07-20 13:35:26
  • 理解Java中的静态绑定和动态绑定

    2022-05-28 04:20:06
  • 详解备忘录模式及其在Java设计模式编程中的实现

    2023-08-24 22:34:02
  • SpringBoot如何进行对象复制的实践

    2023-11-23 03:40:19
  • Java对象在JVM中的生命周期详解

    2023-11-24 16:15:03
  • SpringBoot中的Thymeleaf用法

    2023-10-30 17:30:39
  • 图文并茂讲解RocketMQ消息类别

    2023-06-11 07:59:41
  • java 流与 byte[] 的互转操作

    2023-06-26 11:25:46
  • JavaWeb中使用JavaMail实现发送邮件功能实例详解

    2023-01-07 13:54:37
  • Java Web Fragment在项目中使用方法详解

    2022-04-11 14:17:38
  • C#判断某个软件是否已安装实现代码分享

    2022-07-15 16:34:06
  • Spring @Transactional注解失效解决方案

    2022-10-25 05:30:30
  • java身份证合法性校验并提取身份证有效信息

    2023-04-18 17:26:18
  • Lombok为啥这么牛逼?SpringBoot和IDEA官方都要支持它

    2021-10-18 23:04:50
  • Java 数据结构与算法系列精讲之单向链表

    2023-07-10 08:22:12
  • Mybatis单个参数的if判断报异常There is no getter for property named 'xxx' in 'class java.lang.Integer'的解决方案

    2023-10-16 14:56:01
  • Java线程的start方法回调run方法的操作技巧

    2023-11-11 06:02:00
  • Java中Future和FutureTask的示例详解及使用

    2023-01-29 11:48:42
  • SpringMVC教程之文件上传与下载详解

    2022-12-21 03:49:09
  • Java Thread之Sleep()使用方法及总结

    2023-11-16 10:38:35
  • asp之家 软件编程 m.aspxhome.com