Add AccessFilter layer
This commit is contained in:
parent
673d3b1291
commit
99815b41d8
29
pom.xml
29
pom.xml
|
|
@ -16,7 +16,7 @@
|
|||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<dgs.codegen.version>5.1.17</dgs.codegen.version>
|
||||
|
||||
<spring-cloud.version>2021.0.5</spring-cloud.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
|
@ -28,6 +28,13 @@
|
|||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-dependencies</artifactId>
|
||||
<version>${spring-cloud.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
|
|
@ -35,6 +42,10 @@
|
|||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.graphql.dgs</groupId>
|
||||
<artifactId>graphql-dgs-spring-boot-starter</artifactId>
|
||||
|
|
@ -53,11 +64,27 @@
|
|||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>1.2.78</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.4.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
|||
|
|
@ -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<SignService> signServices;
|
||||
|
||||
@Resource
|
||||
private ApiSignConfigLoad apiSignConfigLoad;
|
||||
|
||||
@Bean
|
||||
public RequestInterceptor requestInterceptor(){
|
||||
final Map<String, SignService> signServiceMap = new HashMap<>();
|
||||
final Map<String, AppSecretInfo> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<AppSecretInfo> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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: 复制输入流</br>
|
||||
*
|
||||
* @param inputStream
|
||||
* @return</br>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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: 复制输入流</br>
|
||||
*
|
||||
* @param inputStream
|
||||
* @return</br>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.longfor.bff_netflix.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AppSecretInfo {
|
||||
|
||||
private String appKey;
|
||||
|
||||
private String appSecret;
|
||||
|
||||
}
|
||||
|
|
@ -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<String, String[]> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.longfor.bff_netflix.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ValidationResult {
|
||||
private int httpStatusCode;
|
||||
private String errorMessage;
|
||||
}
|
||||
|
|
@ -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<SignService> signServices;
|
||||
private final ApiSignConfigLoad apiSignConfigLoad;
|
||||
private final Map<String, SignService> signServiceMap = new HashMap<>();
|
||||
|
||||
private final Map<String, AppSecretInfo> appSecretInfoMap = new HashMap<>();
|
||||
|
||||
public AccessFilter(ApiSignConfigLoad apiSignConfigLoad, List<SignService> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, SignService> signServiceMap;
|
||||
private final Map<String, AppSecretInfo> appSecretInfoMap;
|
||||
|
||||
public RequestInterceptor(Map<String, SignService> signServiceMap, Map<String, AppSecretInfo> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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<String, String[]> urlParams) {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
if (StringUtils.isNotBlank(body)){
|
||||
data.putAll(JSON.parseObject(body, new TypeReference<Map<String, Object>>(){}));
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(urlParams)){
|
||||
urlParams.forEach((key, value) -> data.put(key, value[0]));
|
||||
}
|
||||
return dataToStr(data);
|
||||
}
|
||||
|
||||
private static String dataToStr(Map<String, Object> 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();
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue