这篇文章主要介绍了Spring Cloud Gateway网关XSS过滤方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
这篇文章主要介绍了Spring Cloud Gateway网关XSS过滤方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
这篇文章主要介绍了Spring Cloud Gateway网关XSS过滤方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
XSS是一种经常出现在web应用中的计算机安全漏洞,具体信息请自行Google。本文只分享在Spring Cloud Gateway中执行通用的XSS防范。首次作文,全是代码,若有遗漏不明之处,请各位看官原谅指点。
使用版本
特别注意的是在处理完成之后需要重新构造请求,否则后续业务无法获得参数。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
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")@Datapublic 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; }} |
这里大范围采用Jsoup处理,然后根据自己的业务做了一部分定制。较为特殊的是,我们将字符串中含有'</'标示为这段文本是富文本。在清除xss攻击字符串方法时优化空间较大。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
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("&", "&"); }} |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
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时有问题,原因是使用了新旧代码的问题,现已经将元代码放在正文,新代码如下
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
@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
发表评论