From b6535a2efc2d96d77f0210a4b7083e4ed63dae1a Mon Sep 17 00:00:00 2001 From: lihongjie0209 Date: Tue, 28 Nov 2023 14:09:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=8E=A5=E5=8F=A3=E7=AD=BE?= =?UTF-8?q?=E5=90=8D=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cn/lihongjie/coal/common/Constants.java | 3 + .../lihongjie/coal/common/RequestUtils.java | 18 +++ .../cn/lihongjie/coal/filter/AuthFilter.java | 24 +--- .../cn/lihongjie/coal/filter/CacheFilter.java | 35 +++++ .../cn/lihongjie/coal/filter/SignFilter.java | 131 ++++++++++++++++++ .../sysconfig/service/SysConfigService.java | 1 + 6 files changed, 194 insertions(+), 18 deletions(-) create mode 100644 src/main/java/cn/lihongjie/coal/filter/CacheFilter.java create mode 100644 src/main/java/cn/lihongjie/coal/filter/SignFilter.java diff --git a/src/main/java/cn/lihongjie/coal/common/Constants.java b/src/main/java/cn/lihongjie/coal/common/Constants.java index cb411fd7..b17adf2d 100644 --- a/src/main/java/cn/lihongjie/coal/common/Constants.java +++ b/src/main/java/cn/lihongjie/coal/common/Constants.java @@ -11,9 +11,12 @@ public class Constants { public static final String CACHE_RESOURCE_API_TREE = "resourceApiTree"; public static final String CACHE_RESOURCE_MENU_TREE = "resourceMenuTree"; public static final String HTTP_HEADER_TOKEN = "X-Token"; + public static final String HTTP_HEADER_CLIENT_SIGN = "X-Client-Sign"; + public static final String HTTP_HEADER_TS = "X-TS"; public static final String RATE_LIMIT_GLOBAL_SESSION_PREFIX = "global-session-rl-"; public static final String RATE_LIMIT_GLOBAL_USER_PREFIX = "global-user-rl-"; public static String SYSCONFIG_ENABLE_CAPTCHA = "enable_captcha"; + public static String SYSCONFIG_ENABLE_REQUEST_SIGN = "enable_request_sign"; public static String SYSCONFIG_SESSION_TIMEOUT = "session_timeout"; public static String SYSCONFIG_ACCOUNT_MAX_ONLINE = "account_max_online"; public static String SYSCONFIG_RESETPWD_ENABLE = "resetpwd_enable"; diff --git a/src/main/java/cn/lihongjie/coal/common/RequestUtils.java b/src/main/java/cn/lihongjie/coal/common/RequestUtils.java index 4c86f4a0..3a9c1dde 100644 --- a/src/main/java/cn/lihongjie/coal/common/RequestUtils.java +++ b/src/main/java/cn/lihongjie/coal/common/RequestUtils.java @@ -1,15 +1,23 @@ package cn.lihongjie.coal.common; +import cn.lihongjie.coal.base.dto.R; +import cn.lihongjie.coal.exception.BizException; + +import com.fasterxml.jackson.databind.ObjectMapper; + import eu.bitwalker.useragentutils.Browser; import eu.bitwalker.useragentutils.OperatingSystem; import eu.bitwalker.useragentutils.UserAgent; import eu.bitwalker.useragentutils.Version; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.SneakyThrows; import lombok.experimental.UtilityClass; import org.apache.commons.lang3.StringUtils; +import org.apache.http.entity.ContentType; @UtilityClass public class RequestUtils { @@ -67,4 +75,14 @@ public class RequestUtils { return ""; } } + + @SneakyThrows + public static void writeResponse(BizException ex, HttpServletResponse response) { + + response.setStatus(200); + response.setContentType(ContentType.APPLICATION_JSON.getMimeType()); + R fail = R.fail(ex.getCode(), ex.getMessage()); + response.getOutputStream().write(Ctx.getContext().getBean(ObjectMapper.class).writeValueAsBytes(fail)); + response.getOutputStream().flush(); + } } diff --git a/src/main/java/cn/lihongjie/coal/filter/AuthFilter.java b/src/main/java/cn/lihongjie/coal/filter/AuthFilter.java index 93baa968..cfe4438b 100644 --- a/src/main/java/cn/lihongjie/coal/filter/AuthFilter.java +++ b/src/main/java/cn/lihongjie/coal/filter/AuthFilter.java @@ -1,8 +1,8 @@ package cn.lihongjie.coal.filter; -import cn.lihongjie.coal.base.dto.R; import cn.lihongjie.coal.common.Constants; import cn.lihongjie.coal.common.Ctx; +import cn.lihongjie.coal.common.RequestUtils; import cn.lihongjie.coal.exception.BizException; import cn.lihongjie.coal.permission.dto.PermissionDto; import cn.lihongjie.coal.permission.service.PermissionService; @@ -23,12 +23,9 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.SneakyThrows; - import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.http.entity.ContentType; import org.slf4j.MDC; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -77,7 +74,7 @@ public class AuthFilter extends OncePerRequestFilter { } catch (Exception e) { logger.info("系统异常", e); - writeResponse(new BizException("系统异常"), response); + RequestUtils.writeResponse(new BizException("系统异常"), response); } } @@ -109,7 +106,7 @@ public class AuthFilter extends OncePerRequestFilter { resourceService.findUrlFromCache(getRequestURI(request)); if (resource.isEmpty()) { - writeResponse(new BizException("invalidUrl", "资源未找到"), response); + RequestUtils.writeResponse(new BizException("invalidUrl", "资源未找到"), response); return; } @@ -140,7 +137,7 @@ public class AuthFilter extends OncePerRequestFilter { } else { - writeResponse(new BizException("loginRequired", "请先登录"), response); + RequestUtils.writeResponse(new BizException("loginRequired", "请先登录"), response); } return; @@ -153,7 +150,7 @@ public class AuthFilter extends OncePerRequestFilter { } catch (BizException ex) { - writeResponse(ex, response); + RequestUtils.writeResponse(ex, response); return; } @@ -169,7 +166,7 @@ public class AuthFilter extends OncePerRequestFilter { if (userResource.isEmpty() && BooleanUtils.isFalse(user.getSysAdmin())) { - writeResponse(new BizException("invalidAccess", "当前资源未授权,请联系机构管理员处理。"), response); + RequestUtils.writeResponse(new BizException("invalidAccess", "当前资源未授权,请联系机构管理员处理。"), response); } else { doFilter(request, response, filterChain); @@ -202,13 +199,4 @@ public class AuthFilter extends OncePerRequestFilter { } } - @SneakyThrows - private void writeResponse(BizException ex, HttpServletResponse response) { - - response.setStatus(200); - response.setContentType(ContentType.APPLICATION_JSON.getMimeType()); - R fail = R.fail(ex.getCode(), ex.getMessage()); - response.getOutputStream().write(objectMapper.writeValueAsBytes(fail)); - response.getOutputStream().flush(); - } } diff --git a/src/main/java/cn/lihongjie/coal/filter/CacheFilter.java b/src/main/java/cn/lihongjie/coal/filter/CacheFilter.java new file mode 100644 index 00000000..879240da --- /dev/null +++ b/src/main/java/cn/lihongjie/coal/filter/CacheFilter.java @@ -0,0 +1,35 @@ +package cn.lihongjie.coal.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import java.io.IOException; + +/** +* +*/ +@Component +@Order(0) +@Slf4j +public class CacheFilter extends OncePerRequestFilter { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + + var req = new ContentCachingRequestWrapper(request); + var res = new ContentCachingResponseWrapper(response); + filterChain.doFilter(req, res); + res.copyBodyToResponse(); + + } +} diff --git a/src/main/java/cn/lihongjie/coal/filter/SignFilter.java b/src/main/java/cn/lihongjie/coal/filter/SignFilter.java new file mode 100644 index 00000000..132c55d0 --- /dev/null +++ b/src/main/java/cn/lihongjie/coal/filter/SignFilter.java @@ -0,0 +1,131 @@ +package cn.lihongjie.coal.filter; + +import cn.lihongjie.coal.common.Constants; +import cn.lihongjie.coal.common.RequestUtils; +import cn.lihongjie.coal.exception.BizException; +import cn.lihongjie.coal.sysconfig.service.SysConfigService; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import lombok.extern.slf4j.Slf4j; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.codec.digest.HmacAlgorithms; +import org.apache.commons.codec.digest.HmacUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.*; + +import javax.crypto.Mac; + +/** 请求签名验证 */ +@Component +@Order(10) +@Slf4j +public class SignFilter extends OncePerRequestFilter { + @Autowired ObjectMapper objectMapper; + @Autowired SysConfigService sysConfigService; + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + if (sysConfigService.isEnable(Constants.SYSCONFIG_ENABLE_REQUEST_SIGN)) { + + StringBuilder sb = new StringBuilder(); + + // 请求方法 + + sb.append(StringUtils.defaultString(request.getMethod().toUpperCase())); + sb.append(StringUtils.defaultString("\n")); + + // 请求地址 + sb.append(StringUtils.defaultString(request.getRequestURI())); + sb.append(StringUtils.defaultString("\n")); + + // 请求参数 + sb.append(StringUtils.defaultString(request.getQueryString())); + sb.append(StringUtils.defaultString("\n")); + + // 请求头 + + String token = + ObjectUtils.defaultIfNull( + request.getHeader(Constants.HTTP_HEADER_TOKEN), "anonymous"); + String ts = request.getHeader(Constants.HTTP_HEADER_TS); + if (StringUtils.isEmpty(ts)) { + RequestUtils.writeResponse( + new BizException(Constants.HTTP_HEADER_TS + " 请求头缺失"), response); + return; + } + long tsi = 0; + + try { + + tsi = Long.parseLong(ts); + } catch (Exception e) { + RequestUtils.writeResponse( + new BizException(Constants.HTTP_HEADER_TS + " 请求头格式错误"), response); + return; + } + + long current = System.currentTimeMillis(); + + if (Math.abs(tsi - current) > 1000 * 60 * 2) { + RequestUtils.writeResponse( + new BizException( + Constants.HTTP_HEADER_TS + + " 客户端时间误差过大,请校准时间. 服务器时间为: " + + LocalDateTime.now()), + response); + return; + } + + sb.append(StringUtils.defaultString(token)); + + sb.append(StringUtils.defaultString("\n")); + + sb.append(StringUtils.defaultString(ts)); + + sb.append(StringUtils.defaultString("\n")); + + String sha256Hex = DigestUtils.sha256Hex(request.getInputStream()).toUpperCase(); + + sb.append(StringUtils.defaultString(sha256Hex)); + sb.append(StringUtils.defaultString("\n")); + + Mac mac = + HmacUtils.getInitializedMac( + HmacAlgorithms.HMAC_SHA_256, token.getBytes(StandardCharsets.UTF_8)); + mac.update(sb.toString().getBytes(StandardCharsets.UTF_8)); + String sign = Hex.encodeHexString(mac.doFinal()).toUpperCase(); + + String clientSign = request.getHeader(Constants.HTTP_HEADER_CLIENT_SIGN); + + if (!sign.equals(clientSign)) { + log.debug("key: {} \ndata:{}", token, sb); + log.warn("签名错误: {} {}", clientSign, sign); + RequestUtils.writeResponse( + new BizException(Constants.HTTP_HEADER_CLIENT_SIGN + " 请求头签名错误"), response); + return; + } + } + + doFilter(request, response, filterChain); + } +} diff --git a/src/main/java/cn/lihongjie/coal/sysconfig/service/SysConfigService.java b/src/main/java/cn/lihongjie/coal/sysconfig/service/SysConfigService.java index 77dbd4da..91409403 100644 --- a/src/main/java/cn/lihongjie/coal/sysconfig/service/SysConfigService.java +++ b/src/main/java/cn/lihongjie/coal/sysconfig/service/SysConfigService.java @@ -60,6 +60,7 @@ public class SysConfigService extends BaseService e.getCode(), e -> e)); addDictConfig(all, Constants.SYSCONFIG_ENABLE_CAPTCHA, "验证码状态", "1", "status.type"); + addDictConfig(all, Constants.SYSCONFIG_ENABLE_REQUEST_SIGN, "请求签名验证", "1", "status.type"); addNumberConfig( all,