diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/servlet/RequestLoggingFilter.java b/commons/web-common/src/main/java/com/schbrain/common/web/servlet/RequestLoggingFilter.java index df4e77dba8df545402c46eb60cfa0bdc35d3407f..0fa7f3f4a6dea9185cce5d4969819dae2cbe655e 100644 --- a/commons/web-common/src/main/java/com/schbrain/common/web/servlet/RequestLoggingFilter.java +++ b/commons/web-common/src/main/java/com/schbrain/common/web/servlet/RequestLoggingFilter.java @@ -1,22 +1,19 @@ package com.schbrain.common.web.servlet; import cn.hutool.core.text.CharPool; -import cn.hutool.core.util.ArrayUtil; +import com.schbrain.common.web.utils.ContentCachingServletUtils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.boot.web.servlet.filter.OrderedFilter; import org.springframework.core.Ordered; import org.springframework.web.cors.CorsUtils; import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.web.util.ContentCachingRequestWrapper; -import org.springframework.web.util.WebUtils; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.io.UnsupportedEncodingException; import static com.schbrain.common.web.utils.ContentCachingServletUtils.wrapRequestIfRequired; @@ -58,7 +55,7 @@ public class RequestLoggingFilter extends OncePerRequestFilter implements Ordere String method = request.getMethod(); String requestUri = request.getRequestURI(); String queryString = request.getQueryString(); - String body = getRequestBody(request); + String body = ContentCachingServletUtils.getRequestBody(request, false); StringBuilder builder = new StringBuilder(); builder.append("requestUri: ").append(method).append(CharPool.SPACE).append(requestUri); if (StringUtils.isNotBlank(queryString)) { @@ -73,22 +70,4 @@ public class RequestLoggingFilter extends OncePerRequestFilter implements Ordere return builder.toString(); } - protected String getRequestBody(HttpServletRequest request) { - ContentCachingRequestWrapper nativeRequest = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); - if (nativeRequest == null) { - return null; - } - byte[] content = nativeRequest.getContentAsByteArray(); - if (ArrayUtil.isEmpty(content)) { - return null; - } - String charset = nativeRequest.getCharacterEncoding(); - try { - return new String(content, charset); - } catch (UnsupportedEncodingException e) { - log.warn("unsupported charset {} detect during convert body to String", charset, e); - return null; - } - } - } diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/signature/AbstractSignatureValidationInterceptor.java b/commons/web-common/src/main/java/com/schbrain/common/web/support/signature/AbstractSignatureValidationInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..220770d00101c14680d3b13bfe622277d4986dd9 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/support/signature/AbstractSignatureValidationInterceptor.java @@ -0,0 +1,79 @@ +package com.schbrain.common.web.support.signature; + +import cn.hutool.crypto.digest.DigestUtil; +import com.schbrain.common.web.support.BaseHandlerInterceptor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.method.HandlerMethod; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static cn.hutool.core.text.StrPool.UNDERLINE; +import static com.schbrain.common.web.utils.ContentCachingServletUtils.getRequestBody; +import static com.schbrain.common.web.utils.ContentCachingServletUtils.wrapRequestIfRequired; + +public abstract class AbstractSignatureValidationInterceptor extends BaseHandlerInterceptor { + + private static final String SCH_APP_KEY = "Sch-App-Key"; + private static final String SCH_TIMESTAMP = "Sch-Timestamp"; + private static final String SCH_SIGNATURE = "Sch-Signature"; + private static final String SCH_EXPIRE_TIME = "Sch-Expire-Time"; + + @Override + protected boolean preHandle(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) { + String appKey = request.getHeader(SCH_APP_KEY); + String timestamp = request.getHeader(SCH_TIMESTAMP); + String signature = request.getHeader(SCH_SIGNATURE); + String expireTime = request.getHeader(SCH_EXPIRE_TIME); + + // 空校验 + if (StringUtils.isAnyBlank(appKey, timestamp, signature)) { + throw new SignatureValidationException("签名参数为空!"); + } + + // 过期校验 + if (StringUtils.isNotBlank(expireTime) && System.currentTimeMillis() > Long.parseLong(expireTime)) { + throw new SignatureValidationException("请求信息已过期!"); + } + + // 获取appSecret + SignatureContext context = getSignatureContext(appKey); + if (null == context || StringUtils.isBlank(context.getAppSecret())) { + throw new SignatureValidationException(); + } + + request = wrapRequestIfRequired(request); + // 校验签名 + String requestUri = request.getRequestURI(); + String queryString = request.getQueryString(); + String body = getRequestBody(request, true); + String compareSignature = signParams(requestUri, queryString, body, timestamp, appKey, context.getAppSecret()); + if (!signature.equals(compareSignature)) { + throw new SignatureValidationException(); + } + + SignatureContextUtil.set(context); + return true; + } + + @Override + protected void afterCompletion(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, Exception ex) { + SignatureContextUtil.clear(); + } + + protected abstract T getSignatureContext(String appKey); + + protected String signParams(String requestUri, String queryString, String bodyString, String timestamp, String appKey, String appSecret) { + StringBuilder toSign = new StringBuilder(requestUri); + if (StringUtils.isNotBlank(queryString)) { + toSign.append(UNDERLINE).append(queryString); + } + if (StringUtils.isNotBlank(bodyString)) { + toSign.append(UNDERLINE).append(bodyString); + } + toSign.append(UNDERLINE).append(timestamp).append(UNDERLINE).append(appKey).append(UNDERLINE).append(appSecret); + + return DigestUtil.sha256Hex(toSign.toString()); + } + +} diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/signature/SignatureContext.java b/commons/web-common/src/main/java/com/schbrain/common/web/support/signature/SignatureContext.java new file mode 100644 index 0000000000000000000000000000000000000000..ae96b6472e1ec709271ff68b3a69104fce93e281 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/support/signature/SignatureContext.java @@ -0,0 +1,12 @@ +package com.schbrain.common.web.support.signature; + +import lombok.Data; + +@Data +public class SignatureContext { + + private String appKey; + + private String appSecret; + +} diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/signature/SignatureContextUtil.java b/commons/web-common/src/main/java/com/schbrain/common/web/support/signature/SignatureContextUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..86bdeabc2c1250ea11224ce26e00875b11420f53 --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/support/signature/SignatureContextUtil.java @@ -0,0 +1,33 @@ +package com.schbrain.common.web.support.signature; + +import java.util.Optional; +import java.util.function.Supplier; + +public class SignatureContextUtil { + + private static final ThreadLocal LOCAL = new InheritableThreadLocal<>(); + + private static final Supplier EXCEPTION_SUPPLIER = SignatureValidationException::new; + + /** + * 取值 + */ + public static T get(Class type) { + return type.cast(Optional.ofNullable(LOCAL.get()).orElseThrow(EXCEPTION_SUPPLIER)); + } + + /** + * 赋值 + */ + public static void set(T signatureContext) { + LOCAL.set(signatureContext); + } + + /** + * 移除 + */ + public static void clear() { + LOCAL.remove(); + } + +} diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/signature/SignatureValidationException.java b/commons/web-common/src/main/java/com/schbrain/common/web/support/signature/SignatureValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..51e614240cd1f17b2bc72379760568e42a30071e --- /dev/null +++ b/commons/web-common/src/main/java/com/schbrain/common/web/support/signature/SignatureValidationException.java @@ -0,0 +1,22 @@ +package com.schbrain.common.web.support.signature; + +import com.schbrain.common.exception.BaseException; + +import static com.schbrain.common.constants.ResponseActionConstants.ALERT; +import static com.schbrain.common.constants.ResponseCodeConstants.PARAM_INVALID; + +public class SignatureValidationException extends BaseException { + + private static final long serialVersionUID = 7564001466173362458L; + + private static final String DEFAULT_ERR_MSG = "签名验证异常"; + + public SignatureValidationException() { + this(DEFAULT_ERR_MSG); + } + + public SignatureValidationException(String message) { + super(message, PARAM_INVALID, ALERT); + } + +} diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/utils/ContentCachingServletUtils.java b/commons/web-common/src/main/java/com/schbrain/common/web/utils/ContentCachingServletUtils.java index 6db00fda94cc4fabc8b740cbe036eacb1933a4f0..73d8deeea9dd3a3c7dbfe334a0137a07f5c8bd87 100644 --- a/commons/web-common/src/main/java/com/schbrain/common/web/utils/ContentCachingServletUtils.java +++ b/commons/web-common/src/main/java/com/schbrain/common/web/utils/ContentCachingServletUtils.java @@ -1,16 +1,23 @@ package com.schbrain.common.web.utils; +import cn.hutool.core.util.ArrayUtil; +import lombok.extern.slf4j.Slf4j; import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.web.util.ContentCachingResponseWrapper; +import org.springframework.web.util.WebUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.Charset; /** * @author liaozan * @since 2023-05-08 */ +@Slf4j public class ContentCachingServletUtils { /** @@ -37,4 +44,25 @@ public class ContentCachingServletUtils { } } + public static String getRequestBody(HttpServletRequest request, boolean readFromInputStream) { + ContentCachingRequestWrapper nativeRequest = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); + if (nativeRequest == null) { + return null; + } + Charset charset = Charset.forName(nativeRequest.getCharacterEncoding()); + if (readFromInputStream) { + try { + return StreamUtils.copyToString(request.getInputStream(), charset); + } catch (IOException e) { + log.warn("Failed to read body content from request inputStream"); + return null; + } + } + byte[] content = nativeRequest.getContentAsByteArray(); + if (ArrayUtil.isEmpty(content)) { + return null; + } + return new String(content, charset); + } + }