diff --git a/pom.xml b/pom.xml index 8fc6ead..8ceb4f0 100644 --- a/pom.xml +++ b/pom.xml @@ -1,107 +1,134 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 2.7.9 - - - com.longfor - bff_netflix - 0.0.1-SNAPSHOT - bff_netflix - bff_netflix - - 1.8 - 5.1.17 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.9 + + + com.longfor + bff_netflix + 0.0.1-SNAPSHOT + bff_netflix + bff_netflix + + 1.8 + 5.1.17 + 2021.0.5 + + + + + com.netflix.graphql.dgs + graphql-dgs-platform-dependencies + + 4.9.16 + pom + import + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + com.netflix.graphql.dgs + graphql-dgs-spring-boot-starter + + + com.netflix.graphql.dgs + graphql-dgs-pagination + + + com.netflix.graphql.dgs.codegen + graphql-dgs-codegen-client-core + ${dgs.codegen.version} + + + org.projectlombok + lombok + true + + + org.apache.commons + commons-lang3 + 3.7 + + + com.alibaba + fastjson + 1.2.78 + + + cn.hutool + hutool-all + 5.4.0 + + + org.springframework.boot + spring-boot-starter-test + test + - - - - - com.netflix.graphql.dgs - graphql-dgs-platform-dependencies - - 4.9.16 - pom - import - - - - - - org.springframework.boot - spring-boot-starter - - - com.netflix.graphql.dgs - graphql-dgs-spring-boot-starter - - - com.netflix.graphql.dgs - graphql-dgs-pagination - - - com.netflix.graphql.dgs.codegen - graphql-dgs-codegen-client-core - ${dgs.codegen.version} - - - org.projectlombok - lombok - true - - - org.springframework.boot - spring-boot-starter-test - test - - + - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - io.github.deweyjose - graphqlcodegen-maven-plugin - 1.30 - - - - generate - - - - - true - true - com.longfor - - src/main/resources/schema - - true - true - - - ]]> - - - - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + io.github.deweyjose + graphqlcodegen-maven-plugin + 1.30 + + + + generate + + + + + true + true + com.longfor + + src/main/resources/schema + + true + true + + + ]]> + + + + + + diff --git a/src/main/java/com/longfor/bff_netflix/autoconfigure/ApiSignAutoConfigure.java b/src/main/java/com/longfor/bff_netflix/autoconfigure/ApiSignAutoConfigure.java new file mode 100644 index 0000000..33294d7 --- /dev/null +++ b/src/main/java/com/longfor/bff_netflix/autoconfigure/ApiSignAutoConfigure.java @@ -0,0 +1,48 @@ +package com.longfor.bff_netflix.autoconfigure; + +import com.longfor.bff_netflix.autoconfigure.ApiSignConfigLoad; +import com.longfor.bff_netflix.entity.AppSecretInfo; +import com.longfor.bff_netflix.interceptor.RequestInterceptor; +import com.longfor.bff_netflix.services.SignService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Configuration +@ConditionalOnWebApplication +@ComponentScan(value = {"com.longfor.bff_netflix.*"}) +public class ApiSignAutoConfigure { + + @Resource + private List signServices; + + @Resource + private ApiSignConfigLoad apiSignConfigLoad; + + @Bean + public RequestInterceptor requestInterceptor(){ + final Map signServiceMap = new HashMap<>(); + final Map appSecretInfoMap = new HashMap<>(); + + for (SignService signService : signServices) { + if (StringUtils.isBlank(signService.signMethod())){ + throw new IllegalArgumentException("验签方式配置错误"); + } + signServiceMap.put(signService.signMethod(), signService); + } + for (AppSecretInfo appSecretInfo : apiSignConfigLoad.loadAppSecretInfos()) { + + } + + apiSignConfigLoad.loadAppSecretInfos().forEach(a -> appSecretInfoMap.put(a.getAppKey(), a)); + return new RequestInterceptor(signServiceMap, appSecretInfoMap, apiSignConfigLoad); + } + +} diff --git a/src/main/java/com/longfor/bff_netflix/autoconfigure/ApiSignConfigLoad.java b/src/main/java/com/longfor/bff_netflix/autoconfigure/ApiSignConfigLoad.java new file mode 100644 index 0000000..fe62e1d --- /dev/null +++ b/src/main/java/com/longfor/bff_netflix/autoconfigure/ApiSignConfigLoad.java @@ -0,0 +1,41 @@ +package com.longfor.bff_netflix.autoconfigure; + +import com.alibaba.fastjson.JSONObject; +import com.longfor.bff_netflix.entity.AppSecretInfo; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Configuration +public class ApiSignConfigLoad { + + /** + * 加载调用方应用签名信息 + */ + public List loadAppSecretInfos() { + AppSecretInfo sample = new AppSecretInfo(); + sample.setAppKey("123"); + sample.setAppSecret("kkk"); + return Arrays.asList(sample); + } //TODO: Load the config from Apollo Server + + /** + * 是否验签,默认所有接口都参与验签 + */ + public boolean isSignatureValidationRequired(String path) { + return false; + } + + /** + * 构建业务自定义错误响应,系统提供默认异常实现 + */ + public Object buildErrorResult(String message) { + JSONObject result = new JSONObject(); + result.put("code", -1); + result.put("msg", message); + return result; + } +} diff --git a/src/main/java/com/longfor/bff_netflix/autoconfigure/CachedHttpServletRequestWrapper.java b/src/main/java/com/longfor/bff_netflix/autoconfigure/CachedHttpServletRequestWrapper.java new file mode 100644 index 0000000..38c1bb0 --- /dev/null +++ b/src/main/java/com/longfor/bff_netflix/autoconfigure/CachedHttpServletRequestWrapper.java @@ -0,0 +1,130 @@ +package com.longfor.bff_netflix.autoconfigure; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.*; +import javax.servlet.ServletRequest; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import java.nio.charset.Charset; + +/** + * @author rx6 + */ +public class CachedHttpServletRequestWrapper extends HttpServletRequestWrapper { + private final byte[] cachedContent; + private static Logger log = LoggerFactory.getLogger(CachedHttpServletRequestWrapper.class); + + private String encoding; + + public CachedHttpServletRequestWrapper(HttpServletRequest request, String encoding) throws IOException { + super(request); + this.encoding = StringUtils.isEmpty(encoding) ? "UTF-8" : encoding; + cachedContent = writeToCachedContent(request.getInputStream()); + } + + /** + * 获取请求Body + * + * @return + */ + public String getBodyString() { + StringBuilder sb = new StringBuilder(); + InputStream inputStream = null; + BufferedReader reader = null; + try { + inputStream = new ByteArrayInputStream(cachedContent); + reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName(this.encoding))); + String line = ""; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } catch (IOException e) { + log.error("出错了", e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + log.error("inputStream的io出错了", e); + } + } + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + log.error("读流关闭io出错了", e); + } + } + } + return sb.toString(); + } + + /** + * Description: 复制输入流
+ * + * @param inputStream + * @return
+ */ + public byte[] writeToCachedContent(ServletInputStream inputStream) throws IOException { + try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + int len; + try { + while ((len = inputStream.read(buffer)) > -1) { + byteArrayOutputStream.write(buffer, 0, len); + } + byteArrayOutputStream.flush(); + } catch (IOException e) { + log.error("出错了", e); + } + byte[] bytes = byteArrayOutputStream.toByteArray(); + return bytes; + } + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + return new ContentCachingInputStream(this.cachedContent); + } + + private class ContentCachingInputStream extends ServletInputStream { + + private ByteArrayInputStream inputStream; + + public ContentCachingInputStream(byte[] bytes) { + this.inputStream = new ByteArrayInputStream(bytes); + } + + @Override + public boolean isFinished() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isReady() { + throw new UnsupportedOperationException(); + } + + @Override + public void setReadListener(ReadListener readListener) { + throw new UnsupportedOperationException(); + } + + @Override + public int read() throws IOException { + return inputStream.read(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/longfor/bff_netflix/autoconfigure/CachedServletRequestWrapper.java b/src/main/java/com/longfor/bff_netflix/autoconfigure/CachedServletRequestWrapper.java new file mode 100644 index 0000000..452483d --- /dev/null +++ b/src/main/java/com/longfor/bff_netflix/autoconfigure/CachedServletRequestWrapper.java @@ -0,0 +1,127 @@ +package com.longfor.bff_netflix.autoconfigure; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.*; +import java.nio.charset.Charset; + +/** + * @author rxy + */ +public class CachedServletRequestWrapper extends HttpServletRequestWrapper { + private final byte[] cachedContent; + private static Logger log = LoggerFactory.getLogger(CachedServletRequestWrapper.class); + + private String encoding; + + public CachedServletRequestWrapper(HttpServletRequest request, String encoding) throws IOException { + super(request); + this.encoding = StringUtils.isEmpty(encoding) ? "UTF-8" : encoding; + cachedContent = writeToCachedContent(request.getInputStream()); + } + + /** + * 获取请求Body + * + * @return + */ + public String getBodyString() { + StringBuilder sb = new StringBuilder(); + InputStream inputStream = null; + BufferedReader reader = null; + try { + inputStream = new ByteArrayInputStream(cachedContent); + reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName(this.encoding))); + String line = ""; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } catch (IOException e) { + log.error("出错了", e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + log.error("inputStream的io出错了", e); + } + } + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + log.error("读流关闭io出错了", e); + } + } + } + return sb.toString(); + } + + /** + * Description: 复制输入流
+ * + * @param inputStream + * @return
+ */ + public byte[] writeToCachedContent(ServletInputStream inputStream) throws IOException { + try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + int len; + try { + while ((len = inputStream.read(buffer)) > -1) { + byteArrayOutputStream.write(buffer, 0, len); + } + byteArrayOutputStream.flush(); + } catch (IOException e) { + log.error("出错了", e); + } + byte[] bytes = byteArrayOutputStream.toByteArray(); + return bytes; + } + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + return new ContentCachingInputStream(this.cachedContent); + } + + private class ContentCachingInputStream extends ServletInputStream { + + private ByteArrayInputStream inputStream; + + public ContentCachingInputStream(byte[] bytes) { + this.inputStream = new ByteArrayInputStream(bytes); + } + + @Override + public boolean isFinished() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isReady() { + throw new UnsupportedOperationException(); + } + + @Override + public void setReadListener(ReadListener readListener) { + throw new UnsupportedOperationException(); + } + + @Override + public int read() throws IOException { + return inputStream.read(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/longfor/bff_netflix/autoconfigure/InterceptorConfig.java b/src/main/java/com/longfor/bff_netflix/autoconfigure/InterceptorConfig.java new file mode 100644 index 0000000..f2e8e1a --- /dev/null +++ b/src/main/java/com/longfor/bff_netflix/autoconfigure/InterceptorConfig.java @@ -0,0 +1,20 @@ +package com.longfor.bff_netflix.autoconfigure; + +import com.longfor.bff_netflix.interceptor.RequestInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class InterceptorConfig implements WebMvcConfigurer { + + @Autowired + private RequestInterceptor requestInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { +// registry.addInterceptor(requestInterceptor); + } + +} diff --git a/src/main/java/com/longfor/bff_netflix/datafetchers/ExtentDataFetcher.java b/src/main/java/com/longfor/bff_netflix/datafetchers/ExtentDataFetcher.java index f13e709..dd4d474 100644 --- a/src/main/java/com/longfor/bff_netflix/datafetchers/ExtentDataFetcher.java +++ b/src/main/java/com/longfor/bff_netflix/datafetchers/ExtentDataFetcher.java @@ -27,7 +27,7 @@ public class ExtentDataFetcher { Integer size = dfe.getExecutionStepInfo().getArgument("size"); List extents = this.extentService.extents(); return extents; - // + // // return new SimpleListConnection<>(extents).get(dfe); } diff --git a/src/main/java/com/longfor/bff_netflix/entity/AppSecretInfo.java b/src/main/java/com/longfor/bff_netflix/entity/AppSecretInfo.java new file mode 100644 index 0000000..b8e3614 --- /dev/null +++ b/src/main/java/com/longfor/bff_netflix/entity/AppSecretInfo.java @@ -0,0 +1,12 @@ +package com.longfor.bff_netflix.entity; + +import lombok.Data; + +@Data +public class AppSecretInfo { + + private String appKey; + + private String appSecret; + +} diff --git a/src/main/java/com/longfor/bff_netflix/entity/SignRequest.java b/src/main/java/com/longfor/bff_netflix/entity/SignRequest.java new file mode 100644 index 0000000..2f26980 --- /dev/null +++ b/src/main/java/com/longfor/bff_netflix/entity/SignRequest.java @@ -0,0 +1,49 @@ +package com.longfor.bff_netflix.entity; + +import lombok.Data; +import org.apache.commons.lang3.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +@Data +public class SignRequest { + + private String requestMethod; + + private String path; + + private Map params; + + private String body; + + private String appKey; + + private String sign; + + private String signMethod; + + private String nonce; + + private String timestamp; + + public static SignRequest build(HttpServletRequest request){ + SignRequest signRequest = new SignRequest(); + signRequest.requestMethod = request.getMethod(); + signRequest.path = request.getServletPath(); + signRequest.params = request.getParameterMap(); +// signRequest.body = "" + signRequest.appKey = request.getHeader("x-appkey"); + signRequest.sign = request.getHeader("x-sign"); + signRequest.signMethod = getSignMethod(request); + signRequest.nonce = request.getHeader("x-nonce"); + signRequest.timestamp = request.getHeader("x-timestamp"); + return signRequest; + } + + private static String getSignMethod(HttpServletRequest request){ + final String signMethod = request.getHeader("x-sign-method"); + return StringUtils.isBlank(signMethod) ? "MD5" : signMethod; + } + +} diff --git a/src/main/java/com/longfor/bff_netflix/entity/ValidationResult.java b/src/main/java/com/longfor/bff_netflix/entity/ValidationResult.java new file mode 100644 index 0000000..1278ff8 --- /dev/null +++ b/src/main/java/com/longfor/bff_netflix/entity/ValidationResult.java @@ -0,0 +1,9 @@ +package com.longfor.bff_netflix.entity; + +import lombok.Data; + +@Data +public class ValidationResult { + private int httpStatusCode; + private String errorMessage; +} diff --git a/src/main/java/com/longfor/bff_netflix/filter/AccessFilter.java b/src/main/java/com/longfor/bff_netflix/filter/AccessFilter.java new file mode 100644 index 0000000..9239a14 --- /dev/null +++ b/src/main/java/com/longfor/bff_netflix/filter/AccessFilter.java @@ -0,0 +1,117 @@ +package com.longfor.bff_netflix.filter; + +import com.longfor.bff_netflix.autoconfigure.ApiSignConfigLoad; +import com.longfor.bff_netflix.autoconfigure.CachedHttpServletRequestWrapper; +import com.longfor.bff_netflix.entity.AppSecretInfo; +import com.longfor.bff_netflix.entity.SignRequest; +import com.longfor.bff_netflix.entity.ValidationResult; +import com.longfor.bff_netflix.services.SignService; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.annotation.Resource; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.text.ParseException; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +//@ComponentScan(value = {"com.longfor.bff_netflix.*"}) +public class AccessFilter extends OncePerRequestFilter { + private final List signServices; + private final ApiSignConfigLoad apiSignConfigLoad; + private final Map signServiceMap = new HashMap<>(); + + private final Map appSecretInfoMap = new HashMap<>(); + + public AccessFilter(ApiSignConfigLoad apiSignConfigLoad, List signServices) { + this.apiSignConfigLoad = apiSignConfigLoad; + // 1. Build SignService map + for (SignService signService : signServices) { + signServiceMap.put(signService.signMethod(), signService); + } + + // 2. Build App Secret map + this.apiSignConfigLoad.loadAppSecretInfos().forEach(t -> appSecretInfoMap.put(t.getAppKey(), t)); + this.signServices = signServices; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + CachedHttpServletRequestWrapper wrapper = new CachedHttpServletRequestWrapper(request, "UTF-8"); + String body = wrapper.getBodyString(); + + final SignRequest signRequest = SignRequest.build(request); + signRequest.setBody(body); + + ValidationResult validationResult = validateRequest(signRequest); + if (validationResult.getHttpStatusCode() == HttpServletResponse.SC_OK) { + filterChain.doFilter(wrapper, response); + } else { + response.sendError(validationResult.getHttpStatusCode(), validationResult.getErrorMessage()); + } + + } + + private ValidationResult validateRequest(SignRequest signRequest) { + ValidationResult result = new ValidationResult(); + result.setHttpStatusCode(HttpServletResponse.SC_OK); + + if (!apiSignConfigLoad.isSignatureValidationRequired(signRequest.getPath())) { + return result; + } + + if (!checkRequest(signRequest)) { + result.setHttpStatusCode(HttpServletResponse.SC_BAD_REQUEST); + result.setErrorMessage("Required headers not specified in the request"); + return result; + } + final AppSecretInfo appSecretInfo = appSecretInfoMap.get(signRequest.getAppKey()); + if (appSecretInfo == null) { + result.setHttpStatusCode(HttpServletResponse.SC_BAD_REQUEST); + result.setErrorMessage("无效的应用ID"); + return result; + } + final SignService signService = signServiceMap.get(signRequest.getSignMethod()); + if (signService == null) { + result.setHttpStatusCode(HttpServletResponse.SC_BAD_REQUEST); + result.setErrorMessage("无效的签名方式"); + return result; + } + final String sign = signService.sign(signRequest, appSecretInfo.getAppSecret()); + if (!signRequest.getSign().equals(sign)) { + result.setHttpStatusCode(HttpServletResponse.SC_BAD_REQUEST); + result.setErrorMessage("无效的签名"); + return result; + } + return result; + } + + private boolean checkRequest(SignRequest signRequest) { + if (StringUtils.isAnyBlank(signRequest.getAppKey(), signRequest.getSign(), signRequest.getTimestamp(), signRequest.getNonce())) { +// errorResponse(response, "请求头中缺失用于计算签名的参数"); + return false; + } + try { + final Date tm = DateUtils.parseDate(signRequest.getTimestamp(), "yyyy-MM-dd HH:mm:ss"); + final Date curTime = new Date(); + if (tm.before(DateUtils.addMinutes(curTime, -10)) || tm.after(curTime)) { +// errorResponse(response, "签名时间过期或超期"); + return false; + } + } catch (ParseException exception) { +// errorResponse(response, "时间戳x-timestamp格式有误"); + return false; + } + return true; + } +} diff --git a/src/main/java/com/longfor/bff_netflix/interceptor/RequestInterceptor.java b/src/main/java/com/longfor/bff_netflix/interceptor/RequestInterceptor.java new file mode 100644 index 0000000..762897b --- /dev/null +++ b/src/main/java/com/longfor/bff_netflix/interceptor/RequestInterceptor.java @@ -0,0 +1,116 @@ +package com.longfor.bff_netflix.interceptor; + +import com.alibaba.fastjson.JSON; +import com.longfor.bff_netflix.autoconfigure.ApiSignConfigLoad; +import com.longfor.bff_netflix.autoconfigure.CachedHttpServletRequestWrapper; +import com.longfor.bff_netflix.entity.AppSecretInfo; +import com.longfor.bff_netflix.entity.SignRequest; +import com.longfor.bff_netflix.services.SignService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateUtils; +import org.springframework.http.MediaType; +import org.springframework.util.StreamUtils; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.util.ContentCachingRequestWrapper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.nio.charset.Charset; +import java.text.ParseException; +import java.util.Date; +import java.util.Map; + +@Slf4j +public class RequestInterceptor implements HandlerInterceptor { + + private final ApiSignConfigLoad apiSignConfigLoad; + private final Map signServiceMap; + private final Map appSecretInfoMap; + + public RequestInterceptor(Map signServiceMap, Map appSecretInfoMap, ApiSignConfigLoad apiSignConfigLoad) { + this.signServiceMap = signServiceMap; + this.appSecretInfoMap = appSecretInfoMap; + this.apiSignConfigLoad = apiSignConfigLoad; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { +// final SignRequest signRequest = SignRequest.build(request); +// CachedHttpServletRequestWrapper wrappedRequest = new CachedHttpServletRequestWrapper(request, "UTF-8"); +// BodyReaderHttpServletRequestWrapper wrappedRequest = new BodyReaderHttpServletRequestWrapper(request); +// byte[] bytes = StreamUtils.copyToByteArray(request.getInputStream()); +// String body = new String(bytes); + +// if (!apiSignConfigLoad.isCheckSign(signRequest.getPath())){ +// return true; +// } + return true; + +// if (!checkRequest(signRequest, response)){ +// return false; +// } +// final AppSecretInfo appSecretInfo = appSecretInfoMap.get(signRequest.getAppKey()); +// if (appSecretInfo == null){ +// errorResponse(response, "无效的应用ID"); +// return false; +// } +// final SignService signService = signServiceMap.get(signRequest.getSignMethod()); +// if (signService == null){ +// errorResponse(response, "无效的签名方式"); +// return false; +// } +// final String sign = signService.sign(signRequest, appSecretInfo.getAppSecret()); +// if (!signRequest.getSign().equals(sign)){ +// log.error("请求中的签名值 {} 与系统计算的签名值 {} 不符", signRequest.getSign(), sign); +// errorResponse(response, "无效的签名"); +// return false; +// } +// return true; + } + + /** + * 输出错误响应 + */ + private void errorResponse(HttpServletResponse response, String errorMsg) { + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + try (PrintWriter out = response.getWriter()) { + out.print(JSON.toJSONString(apiSignConfigLoad.buildErrorResult(errorMsg))); + out.flush(); + } catch (Exception e) { + log.error("writeResponse error", e); + } + } + + /** + * 校验请求参数基本的合法性 + */ + private boolean checkRequest(SignRequest signRequest, HttpServletResponse response) { + if (StringUtils.isAnyBlank(signRequest.getAppKey(), signRequest.getSign(), signRequest.getTimestamp(), signRequest.getNonce())) { + errorResponse(response, "请求头中缺失用于计算签名的参数"); + return false; + } + try { + final Date tm = DateUtils.parseDate(signRequest.getTimestamp(), "yyyy-MM-dd HH:mm:ss"); + final Date curTime = new Date(); + if (tm.before(DateUtils.addMinutes(curTime, -10)) || tm.after(curTime)) { + errorResponse(response, "签名时间过期或超期"); + return false; + } + } catch (ParseException exception) { + errorResponse(response, "时间戳x-timestamp格式有误"); + return false; + } + return true; + } + + /** + * TODO 拦截请求重放攻击,后续再开发 + */ + private boolean checkRepeatedNonce(SignRequest signRequest) { + return false; + } + +} diff --git a/src/main/java/com/longfor/bff_netflix/services/Md5SignServiceImpl.java b/src/main/java/com/longfor/bff_netflix/services/Md5SignServiceImpl.java new file mode 100644 index 0000000..15f96e2 --- /dev/null +++ b/src/main/java/com/longfor/bff_netflix/services/Md5SignServiceImpl.java @@ -0,0 +1,23 @@ +package com.longfor.bff_netflix.services; + +import cn.hutool.crypto.digest.MD5; +import com.longfor.bff_netflix.entity.SignRequest; +import com.longfor.bff_netflix.util.ParamToStrUtil; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +@Service +public class Md5SignServiceImpl implements SignService { + + @Override + public String signMethod() { + return "MD5"; + } + + @Override + public String sign(SignRequest signRequest, String secretKey) { + final String bodyStr = ParamToStrUtil.paramToStr(signRequest.getBody(), signRequest.getParams()); + return MD5.create().digestHex(bodyStr + secretKey + signRequest.getTimestamp() + signRequest.getNonce()); + } + +} diff --git a/src/main/java/com/longfor/bff_netflix/services/Sha256SignServiceImpl.java b/src/main/java/com/longfor/bff_netflix/services/Sha256SignServiceImpl.java new file mode 100644 index 0000000..916a58c --- /dev/null +++ b/src/main/java/com/longfor/bff_netflix/services/Sha256SignServiceImpl.java @@ -0,0 +1,24 @@ +package com.longfor.bff_netflix.services; + +import cn.hutool.crypto.digest.HMac; +import cn.hutool.crypto.digest.HmacAlgorithm; +import com.longfor.bff_netflix.entity.SignRequest; +import com.longfor.bff_netflix.util.ParamToStrUtil; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +@Service +public class Sha256SignServiceImpl implements SignService { + + @Override + public String signMethod() { + return "SHA256"; + } + + @Override + public String sign(SignRequest signRequest, String secretKey) { + final String bodyStr = ParamToStrUtil.paramToStr(signRequest.getBody(), signRequest.getParams()); + return new HMac(HmacAlgorithm.HmacSHA256).digestHex(bodyStr + secretKey + signRequest.getTimestamp() + signRequest.getNonce()); + } + +} diff --git a/src/main/java/com/longfor/bff_netflix/services/SignService.java b/src/main/java/com/longfor/bff_netflix/services/SignService.java new file mode 100644 index 0000000..bb03ca4 --- /dev/null +++ b/src/main/java/com/longfor/bff_netflix/services/SignService.java @@ -0,0 +1,17 @@ +package com.longfor.bff_netflix.services; + +import com.longfor.bff_netflix.entity.SignRequest; + +public interface SignService { + + /** + * 签名方式 + */ + String signMethod(); + + /** + * 计算签名 + */ + String sign(SignRequest signRequest, String secretKey); + +} diff --git a/src/main/java/com/longfor/bff_netflix/util/ParamToStrUtil.java b/src/main/java/com/longfor/bff_netflix/util/ParamToStrUtil.java new file mode 100644 index 0000000..a651d2f --- /dev/null +++ b/src/main/java/com/longfor/bff_netflix/util/ParamToStrUtil.java @@ -0,0 +1,49 @@ +package com.longfor.bff_netflix.util; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.CollectionUtils; + +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@Slf4j +public class ParamToStrUtil { + + private ParamToStrUtil(){} + + public static String paramToStr(String body, Map urlParams) { + Map data = new HashMap<>(); + if (StringUtils.isNotBlank(body)){ + data.putAll(JSON.parseObject(body, new TypeReference>(){})); + } + if (!CollectionUtils.isEmpty(urlParams)){ + urlParams.forEach((key, value) -> data.put(key, value[0])); + } + return dataToStr(data); + } + + private static String dataToStr(Map data){ + if (CollectionUtils.isEmpty(data)){ + return ""; + } + StringBuilder sb = new StringBuilder(); + data.entrySet().stream() + .filter(entry -> Objects.nonNull(entry.getValue())) + .sorted(Map.Entry.comparingByKey()) + .forEach(paramEntry -> sb.append(paramEntry.getKey()).append("=").append(paramEntry.getValue()).append("&")); + sb.deleteCharAt(sb.length()-1); + String str; + try { + str = URLEncoder.encode(sb.toString(), "utf-8"); + }catch (Exception e){ + throw new IllegalArgumentException("参数转码异常"); + } + return str.toLowerCase(); + } + +}