Spring Cloud Gateway網(wǎng)關(guān)XSS過濾方式
XSS是一種經(jīng)常出現(xiàn)在web應(yīng)用中的計算機安全漏洞,具體信息請自行Google。本文只分享在Spring Cloud Gateway中執(zhí)行通用的XSS防范。首次作文,全是代碼,若有遺漏不明之處,請各位看官原諒指點。
使用版本
- Spring Cloud版本為 Greenwich.SR4
- Spring Boot版本為 2.1.11.RELEASE
1.創(chuàng)建一個Filter
特別注意的是在處理完成之后需要重新構(gòu)造請求,否則后續(xù)業(yè)務(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過濾 * * @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中的參數(shù) byte[] oldBytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(oldBytes); String bodyString = new String(oldBytes, StandardCharsets.UTF_8); log.debug("原請求參數(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,長度是字節(jié)長度,不是字符串長度 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()); } /** * 字節(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ù)做了一部分定制。較為特殊的是,我們將字符串中含有'</'標示為這段文本是富文本。在清除xss攻擊字符串方法時優(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 { /** * 實例 */ 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數(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 清除后無害的字符串 */ 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ù)錯誤 * * @param richText 是否富文本 * @param afterClean 清理后字符 * @param str 原字符串 * @return true/false */ private boolean paramError(boolean richText, String afterClean, String str) { // 如果包含富文本字符,那么不是參數(shù)錯誤 if (richText) { return false; } // 如果清理后的字符和清理前的字符匹配,那么不是參數(shù)錯誤 if (Objects.equals(str, afterClean)) { return false; } // 如果僅僅包含可以通過的特殊字符,那么不是參數(shù)錯誤 if (Objects.equals(str, this.backSpecialStr(afterClean))) { return false; } // 如果還有...... return true; } /** * 轉(zhuǎn)義回特殊字符 * * @param str 已經(jīng)通過轉(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 { /** * 實例 */ INSTANCE; /** * json對象字符串開始標記 */ private final static String JSON_OBJECT_START = "{"; /** * json對象字符串結(jié)束標記 */ private final static String JSON_OBJECT_END = "}"; /** * json數(shù)組字符串開始標記 */ private final static String JSON_ARRAY_START = "["; /** * json數(shù)組字符串結(jié)束標記 */ 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數(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; } /** * 判斷對象是否是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的指正,在第一步創(chuàng)建Filter時有問題,原因是使用了新舊代碼的問題,現(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,長度是字節(jié)長度,不是字符串長度 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); } }
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
- Spring?cloud網(wǎng)關(guān)gateway進行websocket路由轉(zhuǎn)發(fā)規(guī)則配置過程
- SpringCloud服務(wù)網(wǎng)關(guān)Gateway的使用教程詳解
- SpringCloud gateway+zookeeper實現(xiàn)網(wǎng)關(guān)路由的詳細搭建
- SpringCloud超詳細講解微服務(wù)網(wǎng)關(guān)Gateway
- SpringCloud?GateWay網(wǎng)關(guān)示例代碼詳解
- springcloud gateway網(wǎng)關(guān)服務(wù)啟動報錯的解決
- Spring?Cloud?通過?Gateway?webflux實現(xiàn)網(wǎng)關(guān)異常處理
相關(guān)文章
springboot使用CommandLineRunner解決項目啟動時初始化資源的操作
這篇文章主要介紹了springboot使用CommandLineRunner解決項目啟動時初始化資源的操作,幫助大家更好的理解和學(xué)習(xí)使用springboot框架,感興趣的朋友可以了解下2021-02-02SpringBoot實現(xiàn)統(tǒng)一功能處理的教程詳解
這篇文章主要為大家詳細介紹了SpringBoot如何實現(xiàn)統(tǒng)一功能處理,文中的示例代碼講解詳細,對大家學(xué)習(xí)或工作有一定借鑒價值,感興趣的同學(xué)可以參考閱讀下2023-05-05springMVC實現(xiàn)圖形驗證碼(kaptcha)代碼實例
這篇文章主要介紹了springMVC實現(xiàn)圖形驗證碼(kaptcha)代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值2019-09-09