Spring Cloud Gateway網(wǎng)關(guān)XSS過(guò)濾方式
XSS是一種經(jīng)常出現(xiàn)在web應(yīng)用中的計(jì)算機(jī)安全漏洞,具體信息請(qǐng)自行Google。本文只分享在Spring Cloud Gateway中執(zhí)行通用的XSS防范。首次作文,全是代碼,若有遺漏不明之處,請(qǐng)各位看官原諒指點(diǎn)。
使用版本
- Spring Cloud版本為 Greenwich.SR4
- Spring Boot版本為 2.1.11.RELEASE
1.創(chuàng)建一個(gè)Filter
特別注意的是在處理完成之后需要重新構(gòu)造請(qǐng)求,否則后續(xù)業(yè)務(wù)無(wú)法獲得參數(shù)。
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過(guò)濾
*
* @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請(qǐng)求
if ((HttpMethod.POST.name().equals(method) || HttpMethod.PUT.name().equals(method))) {
return DataBufferUtils.join(request.getBody())
.flatMap(dataBuffer -> {
// 取出body中的參數(shù)
byte[] oldBytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(oldBytes);
String bodyString = new String(oldBytes, StandardCharsets.UTF_8);
log.debug("原請(qǐng)求參數(shù)為:{}", bodyString);
// 執(zhí)行XSS清理
bodyString = XssUtil.INSTANCE.cleanXss(bodyString);
log.debug("修改后參數(shù)為:{}", bodyString);
ServerHttpRequest newRequest = request.mutate().uri(uri).build();
// 重新構(gòu)造body
byte[] newBytes = bodyString.getBytes(StandardCharsets.UTF_8);
DataBuffer bodyDataBuffer = toDataBuffer(newBytes);
Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
// 重新構(gòu)造header
HttpHeaders headers = new HttpHeaders();
headers.putAll(request.getHeaders());
// 由于修改了傳遞參數(shù),需要重新設(shè)置CONTENT_LENGTH,長(zhǎng)度是字節(jié)長(zhǎng)度,不是字符串長(zhǎng)度
int length = newBytes.length;
headers.remove(HttpHeaders.CONTENT_LENGTH);
headers.setContentLength(length);
headers.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=utf8");
// 重寫(xiě)ServerHttpRequestDecorator,修改了body和header,重寫(xiě)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 請(qǐng)求方式
* @return true/false
*/
private boolean white(String url, String method) {
return whiteUrls != null && whiteUrls.contains(XssWhiteUrl.builder().url(url).method(method).build());
}
/**
* 字節(jié)數(shù)組轉(zhuǎn)DataBuffer
*
* @param bytes 字節(jié)數(shù)組
* @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處理,然后根據(jù)自己的業(yè)務(wù)做了一部分定制。較為特殊的是,我們將字符串中含有'</'標(biāo)示為這段文本是富文本。在清除xss攻擊字符串方法時(shí)優(yōu)化空間較大。
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 {
/**
* 實(shí)例
*/
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對(duì)象中的xss攻擊字符
*
* @param val json對(duì)象字符串
* @return 清除后的json對(duì)象字符串
*/
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數(shù)組中的xss攻擊字符
*
* @param val json數(shù)組字符串
* @return 清除后的json數(shù)組字符串
*/
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攻擊字符串,此處優(yōu)化空間較大
*
* @param str 字符串
* @return 清除后無(wú)害的字符串
*/
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, "參數(shù)包含特殊字符");
}
str = richText ? afterClean : this.backSpecialStr(afterClean);
}
return str;
}
/**
* 判斷是否是富文本
*
* @param str 待判斷字符串
* @return true/false
*/
private boolean richText(String str) {
return str.contains(RICH_TEXT);
}
/**
* 判斷是否參數(shù)錯(cuò)誤
*
* @param richText 是否富文本
* @param afterClean 清理后字符
* @param str 原字符串
* @return true/false
*/
private boolean paramError(boolean richText, String afterClean, String str) {
// 如果包含富文本字符,那么不是參數(shù)錯(cuò)誤
if (richText) {
return false;
}
// 如果清理后的字符和清理前的字符匹配,那么不是參數(shù)錯(cuò)誤
if (Objects.equals(str, afterClean)) {
return false;
}
// 如果僅僅包含可以通過(guò)的特殊字符,那么不是參數(shù)錯(cuò)誤
if (Objects.equals(str, this.backSpecialStr(afterClean))) {
return false;
}
// 如果還有......
return true;
}
/**
* 轉(zhuǎn)義回特殊字符
*
* @param str 已經(jīng)通過(guò)轉(zhuǎn)義字符
* @return 轉(zhuǎn)義后特殊字符
*/
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 {
/**
* 實(shí)例
*/
INSTANCE;
/**
* json對(duì)象字符串開(kāi)始標(biāo)記
*/
private final static String JSON_OBJECT_START = "{";
/**
* json對(duì)象字符串結(jié)束標(biāo)記
*/
private final static String JSON_OBJECT_END = "}";
/**
* json數(shù)組字符串開(kāi)始標(biāo)記
*/
private final static String JSON_ARRAY_START = "[";
/**
* json數(shù)組字符串結(jié)束標(biāo)記
*/
private final static String JSON_ARRAY_END = "]";
/**
* 判斷字符串是否json對(duì)象字符串
*
* @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數(shù)組字符串
*
* @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;
}
/**
* 判斷對(duì)象是否是json對(duì)象
*
* @param obj 待判斷對(duì)象
* @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);
}
}
大功告成。
----------------手動(dòng)分隔----------------
修改
感謝@chang_p_x的指正,在第一步創(chuàng)建Filter時(shí)有問(wèn)題,原因是使用了新舊代碼的問(wèn)題,現(xiàn)已經(jīng)將元代碼放在正文,新代碼如下
@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中的參數(shù)
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();
// 執(zhí)行XSS清理
log.debug("{} - [{}:{}] XSS處理前參數(shù):{}", method, uri.getPath(), bodyString);
bodyString = XssUtil.INSTANCE.cleanXss(bodyString);
log.info("{} - [{}:{}] 參數(shù):{}", method, uri.getPath(), bodyString);
ServerHttpRequest newRequest = request.mutate().uri(uri).build();
// 重新構(gòu)造body
byte[] newBytes = bodyString.getBytes(StandardCharsets.UTF_8);
DataBuffer bodyDataBuffer = toDataBuffer(newBytes);
Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
// 重新構(gòu)造header
HttpHeaders headers = new HttpHeaders();
headers.putAll(httpHeaders);
// 由于修改了傳遞參數(shù),需要重新設(shè)置CONTENT_LENGTH,長(zhǎng)度是字節(jié)長(zhǎng)度,不是字符串長(zhǎng)度
int length = newBytes.length;
headers.remove(HttpHeaders.CONTENT_LENGTH);
headers.setContentLength(length);
headers.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=utf8");
// 重寫(xiě)ServerHttpRequestDecorator,修改了body和header,重寫(xiě)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);
}
}
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- Spring?cloud網(wǎng)關(guān)gateway進(jìn)行websocket路由轉(zhuǎn)發(fā)規(guī)則配置過(guò)程
- SpringCloud服務(wù)網(wǎng)關(guān)Gateway的使用教程詳解
- SpringCloud gateway+zookeeper實(shí)現(xiàn)網(wǎng)關(guān)路由的詳細(xì)搭建
- SpringCloud超詳細(xì)講解微服務(wù)網(wǎng)關(guān)Gateway
- SpringCloud?GateWay網(wǎng)關(guān)示例代碼詳解
- springcloud gateway網(wǎng)關(guān)服務(wù)啟動(dòng)報(bào)錯(cuò)的解決
- Spring?Cloud?通過(guò)?Gateway?webflux實(shí)現(xiàn)網(wǎng)關(guān)異常處理
相關(guān)文章
Java回調(diào)函數(shù)實(shí)例代碼詳解
這篇文章主要介紹了Java回調(diào)函數(shù)實(shí)例代碼詳解,需要的朋友可以參考下2017-10-10
自己動(dòng)手編寫(xiě)一個(gè)Mybatis插件之Mybatis脫敏插件
這篇文章主要介紹了自己動(dòng)手編寫(xiě)一個(gè)Mybatis插件之Mybatis脫敏插件,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08
springboot使用CommandLineRunner解決項(xiàng)目啟動(dòng)時(shí)初始化資源的操作
這篇文章主要介紹了springboot使用CommandLineRunner解決項(xiàng)目啟動(dòng)時(shí)初始化資源的操作,幫助大家更好的理解和學(xué)習(xí)使用springboot框架,感興趣的朋友可以了解下2021-02-02
SpringBoot實(shí)現(xiàn)統(tǒng)一功能處理的教程詳解
這篇文章主要為大家詳細(xì)介紹了SpringBoot如何實(shí)現(xiàn)統(tǒng)一功能處理,文中的示例代碼講解詳細(xì),對(duì)大家學(xué)習(xí)或工作有一定借鑒價(jià)值,感興趣的同學(xué)可以參考閱讀下2023-05-05
springMVC實(shí)現(xiàn)圖形驗(yàn)證碼(kaptcha)代碼實(shí)例
這篇文章主要介紹了springMVC實(shí)現(xiàn)圖形驗(yàn)證碼(kaptcha)代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值2019-09-09
解決Spring導(dǎo)出可以運(yùn)行的jar包問(wèn)題
最近需要解決Maven項(xiàng)目導(dǎo)入可執(zhí)行的jar包的問(wèn)題,如果項(xiàng)目不包含Spring,那么使用mvn assembly:assembly即可,這篇文章主要介紹了Spring導(dǎo)出可以運(yùn)行的jar包,需要的朋友可以參考下2023-03-03

