SpringBoot集成拦截器-接口签名的统一验证
- 实现拦截器 继承
HandlerInterceptor
- 拦截器注册到spring容器 实现
WebMvcConfigurer
,添加拦截器即可
demo-web module添加拦截器实现类,接口验签使用MD5
package com.springboot.demo.web.interceptor;
import com.google.gson.Gson;
import com.springboot.demo.common.constants.Constants;
import com.springboot.demo.common.utils.MD5Utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 拦截器 验证签名 目前只支持GET/POST请求
*
* @author zangdaiyang
*/
@Component
@Slf4j
public class SignAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
// 1、获取请求的参数
Map<String, String> params;
String method = request.getMethod();
if (Constants.HTTP_GET.equalsIgnoreCase(method)) {
Map<String, String[]> paramsArr = request.getParameterMap();
if (CollectionUtils.isEmpty(paramsArr)) {
log.warn("Request for get method, param is empty, signature verification failed.");
return false;
}
params = convertParamsArr(paramsArr);
} else if (Constants.HTTP_POST.equalsIgnoreCase(method)) {
// 此处读取了request中的inputStream,因为只能被读取一次,后面spring框架无法读取了,所以需要添加wrapper和filter解决流只能读取一次的问题
BufferedReader reader = request.getReader();
if (reader == null) {
log.warn("Request for post method, body is empty, signature verification failed.");
return false;
}
params = new Gson().fromJson(reader, Map.class);
} else {
log.warn("Not supporting non-get or non-post requests, signature verification failed.");
return false;
}
// 2、验证签名是否匹配
boolean checkSign = params != null && params.getOrDefault(Constants.SIGN_KEY, "").equals(MD5Utils.stringToMD5(params));
log.info("Signature verification ok: {}, URI: {}, method: {}, params: {}", checkSign, request.getRequestURI(), method, params);
return checkSign;
}
private Map<String, String> convertParamsArr(Map<String, String[]> paramsArr) {
Map<String, String> params = new HashMap<>();
for (Map.Entry<String, String[]> entry : paramsArr.entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
return params;
}
}
使用的常量类及MD5工具类
demo-common module中添加工具类
package com.springboot.demo.common.constants;
/**
* 一般常量类
*/
public final class Constants {
/**
* 签名加密前对应的key
*/
public static final String SIGN_ORIGIN_KEY = "origin";
/**
* 签名对应的key
*/
public static final String SIGN_KEY = "sign";
/**
* post请求方法名
*/
public static final String HTTP_POST = "POST";
/**
* get请求方法名
*/
public static final String HTTP_GET = "GET";
private Constants() {}
}
package com.springboot.demo.common.utils;
import com.google.gson.Gson;
import com.springboot.demo.common.constants.Constants;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
public class MD5Utils {
private static final String ALGORITHM_NAME = "md5";
private static final String SECRET_KEY = "dfasuiyhkuhjk2t5290wouojjeerweeqwqdfd";
private static final int RADIX = 16;
private static final int LEN = 32;
private static final String ADD_STR = "0";
/**
* 转换成对应的MD5信息
* @param paramMap
* @return
*/
public static String stringToMD5(Map<String,String> paramMap) {
String covertString = covertParamMapToString(paramMap);
byte[] secretBytes;
try {
secretBytes = MessageDigest.getInstance(ALGORITHM_NAME).digest(
covertString.getBytes());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("No such MD5 Algorithm.");
}
String md5code = new BigInteger(1, secretBytes).toString(RADIX);
for (int i = 0; i < LEN - md5code.length(); i++) {
md5code = ADD_STR + md5code;
}
return md5code;
}
/**
* 转换成对应的string信息
* @param paramMap
* @return
*/
private static String covertParamMapToString(Map<String,String> paramMap) {
Set<String> sets = paramMap.keySet();
List<String> valueList = new ArrayList<>();
for (String key : sets) {
if (key.equals(Constants.SIGN_KEY)) {
continue;
}
String value = paramMap.get(key);
valueList.add(value);
}
// 此处可以使用TreeMap
Collections.sort(valueList);
String jsonString = new Gson().toJson(valueList);
jsonString = jsonString + SECRET_KEY;
return jsonString;
}
}
Post请求body的输入流只能读取一次问题
验证签名时,Get请求可以直接使用request.getParameterMap()来获取参数,之后进行签名验证即可;
如果是post请求,则需要分情况来看:
1、如果post请求使用的是form-data或者x-www-form-urlencoded方式,则也可以通过request.getParameterMap()来获取参数;
2、如果是最常用的json形式(此处post请求场景只考虑此种请求形式的验签),则只能读取post请求中的body(输入流)。
但是,读取了request中的输入流,读取后spring框架再获取request中的body会因为输入流已经被读取报错。
可以通过创建包装类和filter,每次读取后重新赋值body中的输入流来解决。
package com.springboot.demo.web.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
/**
* RequestWrapper 解决POST请求中的BODY参数不能重复读取多次的问题
*
* @author zangdaiyang
* @since 2019.11.08
*/
@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {
private static final int BUFFER_LEN = 128;
private final String body;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[BUFFER_LEN];
int bytesRead;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
}
} catch (IOException ex) {
throw ex;
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ex) {
throw ex;
}
}
}
body = stringBuilder.toString();
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
ServletInputStream servletInputStream = new ServletInputStream() {
public boolean isFinished() {
return false;
}
public boolean isReady() {
return false;
}
public void setReadListener(ReadListener readListener) {}
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
return servletInputStream;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
public String getBody() {
return this.body;
}
}
过滤器实现
- 实现过滤器 继承
Filter
- 注册filter 定义
FilterRegistrationBean
package com.springboot.demo.web.filter;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* 过滤器 以便http post请求body输入流可多次读取
*
* @author zangdaiyang
* @since 2019.11.08
*/
@Component
public class HttpServletFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if (request instanceof HttpServletRequest) {
requestWrapper = new RequestWrapper((HttpServletRequest) request);
}
if (requestWrapper == null) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}
}
}
拦截器及过滤器注册
拦截器、过滤器需要注册到spring容器中才能生效
package com.springboot.demo.web.config;
import com.springboot.demo.web.filter.HttpServletFilter;
import com.springboot.demo.web.interceptor.SignAuthInterceptor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* 配置拦截器与过滤器
*/
@Configuration
public class WebApplicationConfig implements WebMvcConfigurer {
private static final String FILTER_PATH = "/*";
@Resource
private SignAuthInterceptor signAuthInterceptor;
@Resource
private HttpServletFilter httpServletFilter;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(signAuthInterceptor);
}
@Bean
public FilterRegistrationBean registerFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(httpServletFilter);
registration.addUrlPatterns(FILTER_PATH);
registration.setOrder(1);
return registration;
}
}
构建Controller及测试类
package com.springboot.demo.web.controller;
import com.springboot.demo.web.model.ResultInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class DemoController {
@GetMapping("/query")
public ResultInfo query() {
log.info("DemoController query.");
return new ResultInfo();
}
@PostMapping("/clear")
public ResultInfo clear() {
log.info("DemoController clear.");
return new ResultInfo();
}
}
package com.springboot.demo.web;
import com.springboot.demo.common.constants.Constants;
import com.springboot.demo.common.utils.MD5Utils;
import com.springboot.demo.web.model.ResultInfo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@SpringBootTest
class TestController {
private static final String URL_PREFIX = "http://localhost:10095/demo";
private RestTemplate restTemplate = new RestTemplate();
@Test
public void testQuery() {
ResultInfo resultInfo = restTemplate.getForObject(URL_PREFIX + "/query?origin=1&sign=f8a7e51875f63413479d561248398264", ResultInfo.class);
Assert.isTrue(resultInfo.getCode() == 0, "Query Failed");
}
@Test
public void testClear() {
Map<String, String> request = new HashMap<>();
request.put(Constants.SIGN_ORIGIN_KEY, "1");
request.put(Constants.SIGN_KEY, MD5Utils.stringToMD5(request));
ResultInfo resultInfo = restTemplate.postForObject(URL_PREFIX + "/clear", request, ResultInfo.class);
Assert.isTrue(resultInfo.getCode() == 0, "Query Failed");
}
}