Spring Cloud Gateway网关XSS过滤方式
作者:千年的心 发布时间:2021-08-07 13:16:53
标签:SpringCloud,Gateway,网关,过滤
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("&", "&");
}
}
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
0
投稿
猜你喜欢
- 一、@Value读取application.properties配置文件中的值application.properties配置文件fileN
- SpringCloudStream配置以下配置摘自《SpringCloud微服务实战》,配置主要包括两大部分:Stream配置(基础配置、通
- 本文实例总结了java判断字符串是否为数字的方法。分享给大家供大家参考,具体如下:方法一:用JAVA自带的函数public static b
- 背景Java8的stream接口极大地减少了for循环写法的复杂性,stream提供了map/reduce/collect等一系列聚合接口,
- 本文实例为大家分享了java动态导出excel压缩成zip下载的具体代码,供大家参考,具体内容如下package pack.java.io.
- 前言我们在前面介绍AssignAnalyzer时,对AssignAnalyzer.letInit(DiagnosticPosition, V
- 在java中如果我们需要遍历集合并删除其中的某些元素时,例如对于List来说,我们有三种办法。1. 普通的for循环遍历并删除public
- using System; using System.Collections.Generic; using System.Text; usi
- 比如在类上使用该注解 @Alias("dDebtEntity")则在mapper.xml文件中resultType=&q
- Java 序列化和反序列化实例详解在分布式应用中,对象只有经过序列化才能在各个分布式组件之间传输,这就涉及到两个方面的技术-发送者将对象序列
- java Hibernate多对多映射前言:一、单向多对多 单向多对多的例子用人和职位来举例,一个人可以有多个职位
- 以前使用spring的使用要注入property要配置PropertyPlaceholder的bean对象。在springboot除&nbs
- 依赖SpringBoot版本:2.4.2 <dependencies> &
- 前言比较运算符用于判断两个数据的大小,例如:大于、等于、不等于。比较的结果是一个布尔值( true 或 false )。Java 中常用的比
- java "equals"和"==”异同首先简单说一下“equal”和“==”==操作对于基本数据类型比较的是
- 第一种方式:使用@Param注解方式此种方式用法是我们在接口中写方法的参数时,在每个参数的前面加上一个@Param注解即可。该注解有一个va
- 在观察者模式中有2个要素:一个是被观察对象,另一个是观察者。但被观察对象的状态发生改变会通知观察者。举例:把订阅报纸的人看作是观察者,把报纸
- 前言在 上一节 Spring解密 - 默认标签的解析 中,重点分析了 Spring 对默认标签是如何解析的,那么本章继续讲解标签解析,着重讲
- 默认情况下,如果应用以 Android Q 为目标平台,则在访问外部存储设备中的文件时会进入过滤视图。应用可以使用 Context.getE
- 第1部分 ArrayList介绍ArrayList 是一个数组队列,相当于 动态数组。与Java中的数组相比,它的容量能动态增长。它继承于A