Add AccessFilter layer
This commit is contained in:
parent
673d3b1291
commit
99815b41d8
29
pom.xml
29
pom.xml
|
|
@ -16,7 +16,7 @@
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>1.8</java.version>
|
<java.version>1.8</java.version>
|
||||||
<dgs.codegen.version>5.1.17</dgs.codegen.version>
|
<dgs.codegen.version>5.1.17</dgs.codegen.version>
|
||||||
|
<spring-cloud.version>2021.0.5</spring-cloud.version>
|
||||||
</properties>
|
</properties>
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
@ -28,6 +28,13 @@
|
||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</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>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
@ -35,6 +42,10 @@
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter</artifactId>
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.netflix.graphql.dgs</groupId>
|
<groupId>com.netflix.graphql.dgs</groupId>
|
||||||
<artifactId>graphql-dgs-spring-boot-starter</artifactId>
|
<artifactId>graphql-dgs-spring-boot-starter</artifactId>
|
||||||
|
|
@ -53,11 +64,27 @@
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</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>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<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