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");
}
}