|
@@ -0,0 +1,239 @@
|
|
|
+package com.xs.core.filter;
|
|
|
+
|
|
|
+import cn.hutool.core.date.DatePattern;
|
|
|
+import cn.hutool.core.date.LocalDateTimeUtil;
|
|
|
+import com.alibaba.fastjson2.JSON;
|
|
|
+import com.alibaba.fastjson2.JSONObject;
|
|
|
+import com.xs.core.model.ResponseResult;
|
|
|
+import com.xs.core.request.BodyRequestWrapper;
|
|
|
+import com.xs.core.request.RequestWrapper;
|
|
|
+import com.xs.core.utils.SecurityUtil;
|
|
|
+import jakarta.servlet.*;
|
|
|
+import jakarta.servlet.http.HttpServletRequest;
|
|
|
+import jakarta.servlet.http.HttpServletResponse;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.owasp.esapi.ESAPI;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.springframework.web.context.request.RequestContextHolder;
|
|
|
+import org.springframework.web.context.request.ServletRequestAttributes;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.time.Duration;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.util.regex.Pattern;
|
|
|
+
|
|
|
+import static java.util.regex.Pattern.*;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 请求加解密过滤器
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+public class RequestHandler implements Filter {
|
|
|
+
|
|
|
+ @Value("${encryption.isEncryption}")
|
|
|
+ private boolean isEncryption;
|
|
|
+
|
|
|
+ @Value("${encryption.requestPrivateKey}")
|
|
|
+ private String requestPrivateKey;
|
|
|
+
|
|
|
+ @Value("${encryption.responsePublicKey}")
|
|
|
+ private String responsePublicKey;
|
|
|
+
|
|
|
+ @Value("${xssFilter.isOpenXssFilter}")
|
|
|
+ private boolean isOpenXssFilter;
|
|
|
+
|
|
|
+ @Value("${xssFilter.xssFilterWhileUrl}")
|
|
|
+ private String xssFilterWhileUrl;
|
|
|
+
|
|
|
+// @Resource
|
|
|
+// RedisService redisService;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 进行请求加密
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
|
|
|
+ HttpServletRequest request = (HttpServletRequest) servletRequest;
|
|
|
+ HttpServletResponse response = (HttpServletResponse) servletResponse;
|
|
|
+ byte[] results = _getErrorBytes();
|
|
|
+ // 头攻击检测 过滤主机名(非白名单中的直接返回 403)
|
|
|
+ String serverName = request.getServerName();
|
|
|
+ if (serverName == null || !checkBlankList(serverName)) {
|
|
|
+ log.info("加密解析===》[serverName deny access tips]->" + serverName);
|
|
|
+ servletResponse.setContentType("application/json; charset=UTF-8");
|
|
|
+ servletResponse.getOutputStream().write(results);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ request.setAttribute("startTime", System.currentTimeMillis());
|
|
|
+ request.setAttribute("encryptRequestParams", "");
|
|
|
+
|
|
|
+ if (StringUtils.isNotBlank(request.getContentType()) &&
|
|
|
+ request.getContentType().contains("application/json")) {
|
|
|
+ RequestWrapper requestWrapper = new RequestWrapper(request);
|
|
|
+ // 拿到加密串
|
|
|
+ String data = requestWrapper.getBody();
|
|
|
+ if (isEncryption) {
|
|
|
+ if (StringUtils.isBlank(data)) {
|
|
|
+ chain.doFilter(requestWrapper, response);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 开启加密后,需要过滤Knife4j过来的请求
|
|
|
+ ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
|
|
+ HttpServletRequest httpRequest = requestAttributes.getRequest();
|
|
|
+ if (StringUtils.isNotBlank(httpRequest.getHeader("Knife4j-Gateway-Code"))) {
|
|
|
+ chain.doFilter(requestWrapper, response);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ data = data.replaceAll("\\s", "");
|
|
|
+
|
|
|
+ request.setAttribute("encryptRequestParams", data);
|
|
|
+
|
|
|
+ JSONObject json = JSONObject.parseObject(data);
|
|
|
+ // 验证签名
|
|
|
+ if (!SecurityUtil.verifySign(json.getString("encryptData"), json.getString("identifying"))) {
|
|
|
+ log.error("加密解析===》签名有误!");
|
|
|
+ servletResponse.setContentType("application/json; charset=UTF-8");
|
|
|
+ servletResponse.getOutputStream().write(results);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 解密
|
|
|
+ // 临时处理,后续删除
|
|
|
+ String source = json.getString("encryptData");
|
|
|
+ if (source.startsWith("04")) {
|
|
|
+ source = source.substring(2);
|
|
|
+ }
|
|
|
+ String result = SecurityUtil.decryptSM2(requestPrivateKey, source);
|
|
|
+ // 请求有效性校验
|
|
|
+ JSONObject requestJson = JSONObject.parseObject(result);
|
|
|
+ if (!requestJson.containsKey("basicData")) {
|
|
|
+ log.error("加密解析===》请求参数有误!");
|
|
|
+ servletResponse.setContentType("application/json; charset=UTF-8");
|
|
|
+ servletResponse.getOutputStream().write(results);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ JSONObject basicJson = requestJson.getJSONObject("basicData");
|
|
|
+ if (!basicJson.containsKey("timeStamp") || !basicJson.containsKey("messageId")) {
|
|
|
+ log.error("加密解析===》请求参数有误!");
|
|
|
+ servletResponse.setContentType("application/json; charset=UTF-8");
|
|
|
+ servletResponse.getOutputStream().write(results);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 比对时间
|
|
|
+ LocalDateTime currTime = LocalDateTime.now();
|
|
|
+ LocalDateTime targetTime = LocalDateTimeUtil.parse(basicJson.getString("timeStamp"), DatePattern.PURE_DATETIME_PATTERN);
|
|
|
+ Duration between = LocalDateTimeUtil.between(currTime, targetTime);
|
|
|
+ long minutes = between.toMinutes();
|
|
|
+ if (minutes > 5L) {
|
|
|
+ log.error("加密解析===》更新当前设备时间为北京时间!");
|
|
|
+ servletResponse.setContentType("application/json; charset=UTF-8");
|
|
|
+ servletResponse.getOutputStream().write(results);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 验证请求唯一性
|
|
|
+ // --------
|
|
|
+ // 需要优化
|
|
|
+// String key = String.format("%s%s", ConstantConfig.MESSAGE_ID_PREFIX, basicJson.getString("messageId"));
|
|
|
+// if (redisService.hasKey(key)) {
|
|
|
+// log.error("加密解析===》已经接收过该请求!");
|
|
|
+// servletResponse.setContentType("application/json; charset=UTF-8");
|
|
|
+// servletResponse.getOutputStream().write(results);
|
|
|
+// return;
|
|
|
+// }
|
|
|
+// redisService.setNx(key, "0", 60 * 60 * 8);
|
|
|
+ // --------
|
|
|
+ if (!requestJson.containsKey("bizData") ||
|
|
|
+ StringUtils.isBlank(requestJson.getString("bizData"))) {
|
|
|
+ request = new BodyRequestWrapper(request, "[]");
|
|
|
+ } else {
|
|
|
+ request = new BodyRequestWrapper(request, _cleanXSS(requestJson.getString("bizData"), request));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ request = new BodyRequestWrapper(request, _cleanXSS(data, request));
|
|
|
+ }
|
|
|
+ chain.doFilter(request, response);
|
|
|
+ } else {
|
|
|
+ chain.doFilter(request, response);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String _cleanXSS(String value, HttpServletRequest request) {
|
|
|
+ if (isOpenXssFilter && null != value &&
|
|
|
+ _judgeUrl(request.getRequestURI().replaceAll(request.getContextPath(), ""))) {
|
|
|
+ ESAPI.initialize("org.owasp.esapi.reference.DefaultSecurityConfiguration");
|
|
|
+ // 推荐使用ESAPI库来避免脚本攻击
|
|
|
+ value = ESAPI.encoder().canonicalize(value);
|
|
|
+ // 避免空字符串
|
|
|
+ value = value.replaceAll("", "");
|
|
|
+ // 避免script 标签
|
|
|
+ Pattern scriptPattern = compile("<script>(.*?)</script>", CASE_INSENSITIVE);
|
|
|
+ value = scriptPattern.matcher(value).replaceAll("");
|
|
|
+ // 避免src形式的表达式
|
|
|
+ scriptPattern = compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"", CASE_INSENSITIVE | MULTILINE | DOTALL);
|
|
|
+ value = scriptPattern.matcher(value).replaceAll("");
|
|
|
+ // 删除单个的 </script> 标签
|
|
|
+ scriptPattern = compile("</script>", CASE_INSENSITIVE);
|
|
|
+ value = scriptPattern.matcher(value).replaceAll("");
|
|
|
+ // 删除单个的<script ...> 标签
|
|
|
+ scriptPattern = compile("<script(.*?)>", CASE_INSENSITIVE | MULTILINE | DOTALL);
|
|
|
+ value = scriptPattern.matcher(value).replaceAll("");
|
|
|
+ // 避免 eval(...) 形式表达式
|
|
|
+ scriptPattern = compile("eval\\((.*?)\\)", CASE_INSENSITIVE | MULTILINE | DOTALL);
|
|
|
+ value = scriptPattern.matcher(value).replaceAll("");
|
|
|
+ // 避免 expression(...) 表达式
|
|
|
+ scriptPattern = compile("expression\\((.*?)\\)", CASE_INSENSITIVE | MULTILINE | DOTALL);
|
|
|
+ value = scriptPattern.matcher(value).replaceAll("");
|
|
|
+ // 避免 javascript: 表达式
|
|
|
+ scriptPattern = compile("javascript:", CASE_INSENSITIVE);
|
|
|
+ value = scriptPattern.matcher(value).replaceAll("");
|
|
|
+ // 避免 vbscript: 表达式
|
|
|
+ scriptPattern = compile("vbscript:", CASE_INSENSITIVE);
|
|
|
+ value = scriptPattern.matcher(value).replaceAll("");
|
|
|
+ // 避免 onload= 表达式
|
|
|
+ scriptPattern = compile("onload(.*?)=", CASE_INSENSITIVE | MULTILINE | DOTALL);
|
|
|
+ value = scriptPattern.matcher(value).replaceAll("");
|
|
|
+ // 避免 onXX= 表达式
|
|
|
+ scriptPattern = compile("on.*(.*?)=", CASE_INSENSITIVE | MULTILINE | DOTALL);
|
|
|
+ value = scriptPattern.matcher(value).replaceAll("");
|
|
|
+ }
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean _judgeUrl(String url) {
|
|
|
+ if (StringUtils.isNotBlank(xssFilterWhileUrl)) {
|
|
|
+ String[] whiteUrls = xssFilterWhileUrl.split(",");
|
|
|
+ for (String whiteUrl : whiteUrls) {
|
|
|
+ if (url.startsWith(whiteUrl.replaceAll("\\*", ""))) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private byte[] _getErrorBytes() {
|
|
|
+ if (isEncryption) {
|
|
|
+ String result = SecurityUtil.encryptSM2(responsePublicKey, JSON.toJSONString(ResponseResult.failed("error!")));
|
|
|
+ String sign = SecurityUtil.getSign(result);
|
|
|
+ JSONObject jsonObject = new JSONObject();
|
|
|
+ jsonObject.put("encryptData", result);
|
|
|
+ jsonObject.put("identifying", sign);
|
|
|
+ return jsonObject.toJSONString().getBytes(StandardCharsets.UTF_8);
|
|
|
+ } else {
|
|
|
+ return JSON.toJSONString(ResponseResult.failed("error!")).getBytes(StandardCharsets.UTF_8);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ //判断主机是否存在白名单中
|
|
|
+ private boolean checkBlankList(String serverName) {
|
|
|
+// if(serverName.equals("127.0.0.1"||serverName.equals("localhost")){//此处为自己网站的主机地址
|
|
|
+// return true;
|
|
|
+// }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+}
|