数据签名,主要就是为了防止 数据被 篡改
避免body 只读一次
@Component
@WebFilter(filterName = "httpServletRequestWrapperFilter", urlPatterns = {"/*"})
public class RepeatReadFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 防止流读取一次后就没有了, 所以需要将流继续写出去
String contentType = request.getContentType();
if(StringUtil.isNotEmpty(contentType) && contentType.contains(MediaType.APPLICATION_JSON_VALUE)){
ServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(httpServletRequest);
chain.doFilter(requestWrapper, response);
}else {
chain.doFilter(request,response);
}
}
@Override
public void destroy() {
}
}
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
String sessionStream = getBodyString(request);
body = sessionStream.getBytes(Charset.forName("UTF-8"));
}
/**
* 获取请求Body
*
* @param request
* @return
*/
public String getBodyString(final ServletRequest request) {
StringBuilder sb = new StringBuilder();
try (InputStream inputStream = cloneInputStream(request.getInputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
/**
* Description: 复制输入流</br>
*
* @param inputStream
* @return</br>
*/
public InputStream cloneInputStream(ServletInputStream inputStream) {
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) {
e.printStackTrace();
}
return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
拦截器
@Slf4j
@Component
public class SignAuthInterceptor implements HandlerInterceptor {
/**
* 时间戳
*/
private static final String TIMESTAMP = "timeStamp";
/**
* 应用 appKey
*/
private static final String APP_KEY = "appKey";
/**
* 签名 sign
*/
private static final String SIGN = "sign";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 非控制器请求直接跳出
if (!(handler instanceof HandlerMethod)) {
return true;
}
String contentType = request.getContentType();
SortedMap<String, String> result = new TreeMap<>();
if(StringUtil.isNotEmpty(contentType) && contentType.contains(MediaType.APPLICATION_JSON_VALUE)){
HttpUtils.getBodyParam(request,result);
}else {
HttpUtils.getFormParams(request,result);
}
// 校验时间戳
String timestamp = result.get(TIMESTAMP);
String appKey = result.get(APP_KEY);
String sign = result.get(SIGN);
if(StringUtil.isEmpty(timestamp)){
throw new BusinessException("参数错误 缺失[timeStamp]");
}
if(StringUtil.isEmpty(appKey)){
throw new BusinessException("参数错误 缺失[appKey]");
}
if(StringUtil.isEmpty(sign)){
throw new BusinessException("参数错误 缺失[sign]");
}
boolean validateTimeStamp = validateTimeStamp(Long.valueOf(timestamp));
if(!validateTimeStamp){
throw new BusinessException("当前请求参数已过期,不允许访问");
}
//校验签名
boolean verifySign = SignUtils.verifySign(result,appSecret);
if(!verifySign){
log.info("签名 app_secret:{}",appSecret);
throw new BusinessException("签名不正确,不允许访问");
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
/**
* @description: 判断客户端的请求是否超过3分钟
*/
private boolean validateTimeStamp(long timestamp) {
Long tims = (System.currentTimeMillis()-timestamp) / (1000 * 60);
//验证时间戳是否超过3分钟
if (Math.abs(tims) > 3) {
return false;
} else {
return true;
}
}
}
获取参数方法
public class HttpUtils {
/**
*
* @param request
* @return
* @throws IOException
*/
public static SortedMap<String, String> getAllParams(HttpServletRequest request) throws IOException {
SortedMap<String, String> result = new TreeMap<>();
// 获取URL上的参数
getUrlParams(request, result);
// 获取body参数
getBodyParam(request, result);
return result;
}
/**
* 获取 Body 参数
*
*/
public static void getBodyParam(final HttpServletRequest request, SortedMap<String, String> result)
throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
String str = "";
StringBuilder wholeStr = new StringBuilder();
// 一行一行的读取body体里面的内容;
while ((str = reader.readLine()) != null) {
wholeStr.append(str);
}
wholeStr.trimToSize();
String s = wholeStr.toString();
if (!StringUtil.isEmpty(s)) {
// 转化成json对象
Map<String, String> allRequestParam = JSONObject.parseObject(s, Map.class);
// 将URL的参数和body参数进行合并
for (Map.Entry entry : allRequestParam.entrySet()) {
result.put((String)entry.getKey(), (String)entry.getValue());
}
}
}
/**
* 获取url参数
*/
public static void getUrlParams(HttpServletRequest request, SortedMap<String, String> result) {
String param = "";
try {
String urlParam = request.getQueryString();
if (urlParam != null) {
param = URLDecoder.decode(urlParam, "utf-8");
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String[] params = param.split("&");
for (String s : params) {
int index = s.indexOf("=");
if (index != -1) {
result.put(s.substring(0, index), s.substring(index + 1));
}
}
}
/**
* 获取表单数据
*/
public static void getFormParams(HttpServletRequest request, SortedMap<String, String> result) {
Map<String, String[]> parameterMap = request.getParameterMap();
for (Map.Entry<String,String[]> entry:parameterMap.entrySet()){
result.put(entry.getKey(), Arrays.stream(entry.getValue()).collect(Collectors.joining(",")));
}
}
}
签名工具
算法 参考微信 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3
@Slf4j
public class SignUtils {
/**
* @param params
* 所有的请求参数都会在这里进行排序加密
* @return 验证签名结果
*/
public static boolean verifySign(SortedMap<String, String> params,String key) {
String urlSign = params.get("sign");
log.info("Url Sign : {}", urlSign);
if (StringUtil.isEmpty(urlSign)) {
return false;
}
// 把参数加密
String paramsSign = getParamsSign(params,key);
log.info("Param Sign : {}", paramsSign);
return !StringUtil.isEmpty(paramsSign) && urlSign.equals(paramsSign);
}
/**
* @param params
*
* @return 得到签名
*/
public static String getParamsSign(SortedMap<String, String> params,String key) {
// 要先去掉 Url 里的 Sign
params.remove("sign");
StringBuffer sb = new StringBuffer();
params.forEach((k,v)->{
sb.append(k).append("=").append(v).append("&");
});
sb.append("key").append("=").append(key);
try {
String sign = new String(sb.toString().getBytes(), "utf-8");
return SecureUtil.md5(sign).toUpperCase();
} catch (UnsupportedEncodingException e) {
throw new BusinessException("签名错误不支持utf-8编码");
}
}
}
彩蛋 也可以在网关 做,附上网关 获取参数 的类。
定义 filter 存到上下文
package com.xiaominfo.swagger.service.doc.config.test02;
import io.netty.buffer.ByteBufAllocator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
/**
* @Author
* @Date 2022/6/20 11:56
*/
@Component
@Slf4j
public class RequestCoverFilter implements GlobalFilter, Ordered {
/**
* default HttpMessageReader
*/
private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
/**
* ReadFormData
*
* @param exchange
* @param chain
* @return
*/
private Mono<Void> readFormData(ServerWebExchange exchange, GatewayFilterChain chain,
GatewayContext gatewayContext) {
final ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
return exchange.getFormData().doOnNext(multiValueMap -> {
gatewayContext.setFormData(multiValueMap);
log.debug("[GatewayContext]Read FormData:{}", multiValueMap);
}).then(Mono.defer(() -> {
Charset charset = headers.getContentType().getCharset();
charset = charset == null ? StandardCharsets.UTF_8 : charset;
String charsetName = charset.name();
MultiValueMap<String, String> formData = gatewayContext.getFormData();
/**
* formData is empty just return
*/
if (null == formData || formData.isEmpty()) {
return chain.filter(exchange);
}
StringBuilder formDataBodyBuilder = new StringBuilder();
String entryKey;
List<String> entryValue;
try {
/**
* repackage form data
*/
for (Map.Entry<String, List<String>> entry : formData.entrySet()) {
entryKey = entry.getKey();
entryValue = entry.getValue();
if (entryValue.size() > 1) {
for (String value : entryValue) {
formDataBodyBuilder.append(entryKey).append("=")
.append(URLEncoder.encode(value, charsetName)).append("&");
}
} else {
formDataBodyBuilder.append(entryKey).append("=")
.append(URLEncoder.encode(entryValue.get(0), charsetName)).append("&");
}
}
} catch (UnsupportedEncodingException e) {
// ignore URLEncode Exception
}
/**
* substring with the last char '&'
*/
String formDataBodyString = "";
if (formDataBodyBuilder.length() > 0) {
formDataBodyString = formDataBodyBuilder.substring(0, formDataBodyBuilder.length() - 1);
}
/**
* get data bytes
*/
byte[] bodyBytes = formDataBodyString.getBytes(charset);
int contentLength = bodyBytes.length;
ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(request) {
/**
* change content-length
*
* @return
*/
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
/**
* read bytes to Flux<Databuffer>
*
* @return
*/
@Override
public Flux<DataBuffer> getBody() {
return DataBufferUtils.read(new ByteArrayResource(bodyBytes),
new NettyDataBufferFactory(ByteBufAllocator.DEFAULT), contentLength);
}
};
ServerWebExchange mutateExchange = exchange.mutate().request(decorator).build();
log.info("[GatewayContext]Rewrite Form Data :{}", formDataBodyString);
return chain.filter(mutateExchange);
}));
}
/**
* ReadJsonBody
*
* @param exchange
* @param chain
* @return
*/
private Mono<Void> readBody(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext) {
/**
* join the body
*/
return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
/*
* read the body Flux<DataBuffer>, and release the buffer
* //TODO when SpringCloudGateway Version Release To G.SR2,this can be update with the new version's feature
* see PR https://github.com/spring-cloud/spring-cloud-gateway/pull/1095
*/
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
DataBufferUtils.retain(buffer);
return Mono.just(buffer);
});
/**
* repackage ServerHttpRequest
*/
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
/**
* mutate exchage with new ServerHttpRequest
*/
ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
/**
* read body string with default messageReaders
*/
return ServerRequest.create(mutatedExchange, messageReaders).bodyToMono(String.class)
.doOnNext(objectValue -> {
gatewayContext.setCacheBody(objectValue);
log.debug("[GatewayContext]Read JsonBody:{}", objectValue);
}).then(chain.filter(mutatedExchange));
});
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
/**
* save request path and serviceId into gateway context
*/
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
GatewayContext gatewayContext = new GatewayContext();
String path = request.getPath().pathWithinApplication().value();
gatewayContext.setPath(path);
gatewayContext.getFormData().addAll(request.getQueryParams());
gatewayContext.setIpAddress(String.valueOf(request.getRemoteAddress()));
HttpHeaders headers = request.getHeaders();
gatewayContext.setHeaders(headers);
log.debug("HttpMethod:{},Url:{}", request.getMethod(), request.getURI().getRawPath());
/// 注意,因为webflux的响应式编程 不能再采取原先的编码方式 即应该先将gatewayContext放入exchange中,否则其他地方可能取不到
/**
* save gateway context into exchange
*/
exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT, gatewayContext);
// 处理参数
MediaType contentType = headers.getContentType();
long contentLength = headers.getContentLength();
if (contentLength > 0) {
if (MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_JSON_UTF8.equals(contentType)) {
return readBody(exchange, chain, gatewayContext);
}
if (MediaType.APPLICATION_FORM_URLENCODED.equals(contentType)) {
return readFormData(exchange, chain, gatewayContext);
}
}
// TODO 多版本划区域控制后期实现
log.debug("[GatewayContext]ContentType:{},Gateway context is set with {}", contentType, gatewayContext);
return chain.filter(exchange);
}
}
package com.xiaominfo.swagger.service.doc.config.test02;
import lombok.Data;
import org.springframework.http.HttpHeaders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* @Author
* @Date 2022/6/20 11:55
*/
@Data
public class GatewayContext {
public static final String CACHE_GATEWAY_CONTEXT = "cacheGatewayContext";
/**
* cache headers
*/
private HttpHeaders headers;
/**
* baseHeader
*/
//private BaseHeader baseHeader;
/**
* cache json body
*/
private String cacheBody;
/**
* cache formdata
*/
private MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
/**
* ipAddress
*/
private String ipAddress;
/**
* path
*/
private String path;
}
// 其他 filter 从上下文获取
GatewayContext gatewayContext = exchange.getAttribute(GatewayContext.CACHE_GATEWAY_CONTEXT);
网友评论