思路:
为了防止报文中途被拦截修改,需要在请求头中加入签名字段信息,值为请求连接请求方式params参数post参数认证信息等字符串的加密字符串。
加密方式采用非对称加密(不可逆)。 后端获取到这些数据后通过同样的加密方式对上述字符串进行加密,然后与签名字段做对比,如果不一致则认为报文信息被修改过,则拦截此次请求。
此种方式需要加密前的字符串需要保持一致,且加密方式保持一致。
请求params参数保持一致
参数转化为字符串前要先排序:
前端排序方法:
const sortJSON = (old) => {
const type = Object.prototype.toString.call(old)
const res = {}
// 如果是对象就对key排序,生成一个新的对象返回
if (type === '[object Object]') {
const keyArray = []
for (const key in old) {
keyArray.push(key)
}
keyArray.sort()
for (let key in keyArray) {
key = keyArray[key]
const value = '' + old[key]
res[key] = sortJSON(value)
}
return res
}
if (type === '[object Array]') {
const type = Object.prototype.toString.call(old[0])
// 如果数组里嵌套字符串和数字,直接对数组排序
if (type === '[object String]' || type === '[object Number]') {
old.sort()
return old
}
// 如果数组里嵌套对象,不改变对象顺序,只改变对象内属性顺序
const newArray = []
for (let i = 0; i < old.length; i++) {
newArray.push(sortJSON(old[i]))
}
return newArray
}
// 对对象里的value排序,但不是对象活数组,就原样返回
return old
}
使用:
params = JSON.stringify(sortJSON(params))
后端通过httpservletrequest获得参数放入map中,然后根据key排序,
post报文保持一致:
post报文是通过数据流传输的,因此不用排序。后端在获取到数据后,转化为字符串即可(此处要排除二进制文件,以及乱码问题)。
因为流数据只能读取一次,所以需要继承HttpServletRequestWrapper重写方法。(百度,多次读取httpservletrequest流数据)
@Slf4j
public class KCHttpRequestWrapper extends HttpServletRequestWrapper {
private byte[] requestBody = null;
public KCHttpRequestWrapper(HttpServletRequest request) {
super(request);
try {
requestBody = StreamUtils.copyToByteArray(request.getInputStream());
} catch (IOException e) {
log.error("Wrap requestBody failed");
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (requestBody == null) {
requestBody = new byte[0];
}
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
@Override
public void setReadListener(ReadListener listener) {
// do nothing
}
@Override
public boolean isReady() {
return false;
}
@Override
public boolean isFinished() {
return false;
}
};
}
@Override//对外提供读取流的方法
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(),"UTF-8"));
}
}
public class ChannelFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if (servletRequest instanceof HttpServletRequest) {
String contentType = servletRequest.getContentType();
String method = "multipart/form-data";
if (contentType != null && contentType.contains(method)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
requestWrapper = new KCHttpRequestWrapper((HttpServletRequest) servletRequest);
}
if (requestWrapper == null) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
filterChain.doFilter(requestWrapper, servletResponse);
}
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
@Slf4j
public class SignatureInterceptor extends HandlerInterceptorAdapter {
private final static String SALT = "testtest";
@Override
public boolean preHandle(HttpServletRequest servletRequest, HttpServletResponse response, Object handler) throws IOException {
String contentType = servletRequest.getContentType();
if (null != contentType && contentType.contains("multipart/form-data")) {
return true;
}
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
JwtIgnore jwtIgnore = handlerMethod.getMethodAnnotation(JwtIgnore.class);
if (jwtIgnore != null) {
// 忽略带JwtIgnore注解的请求
return true;
}
}
HttpServletRequest request = new KCHttpRequestWrapper(servletRequest);
if (request == null) {
return true;
}
String method = request.getMethod().toLowerCase();
if ("options".equals(method)) {
return true;
}
String path = request.getServletPath();
if (!path.contains("api")) {
return true;
}
String frontSignature = request.getHeader("Signature");
// if (null == frontSignature) {
// return true;
// }
StringBuffer newSignatureBuffer = new StringBuffer(SALT);
newSignatureBuffer.append(request.getRequestURI());
newSignatureBuffer.append(method);
Map<String, String[]> parameterMap = request.getParameterMap();
newSignatureBuffer.append(dealParams(parameterMap));
String data = "";
StringBuffer sb = new StringBuffer();
BufferedReader isr = request.getReader();
try {
char[] charBuffer = new char[2048];
int readCount = 0;
while ((readCount = isr.read(charBuffer)) != -1) {
sb.append(charBuffer, 0, readCount);
}
} catch (IOException e) {
throw e;
}
data = sb.toString();
newSignatureBuffer.append(data);
String authorization = request.getHeader("Authorization");
if (!StringUtils.hasLength(authorization)) {
authorization = "";
}
newSignatureBuffer.append(authorization);
String decryptData = DigestUtils.sha256Hex(newSignatureBuffer.toString());
if (!StringUtils.hasLength(frontSignature)) {
log.info("报文头没有Signature,请求不被允许!");
sendError(response, "没有Signature,请求不被允许");
} else if (frontSignature.equals(decryptData)) {
return true;
} else {
log.info("报文有被修改,请求不被允许!请求信息:{}。原始报文密文:{}", newSignatureBuffer, frontSignature);
sendError(response, "报文修改,请求不被允许");
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 放置tl内存泄漏,手动回收
WebContext.getInstance().removeCurrentUser();
}
private void sendError(HttpServletResponse response, String errMsg) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_OK);
ResultVO<String> vo = ResultUtil.error(HttpServletResponse.SC_UNAUTHORIZED, errMsg);
response.getWriter().println(JSON.toJSONString(vo));
}
/**
* 处理params参数,与前端保持一致
*
* @param parameterMap
* @return
*/
private String dealParams(Map<String, String[]> parameterMap) {
String params = "";
if (CollectionUtils.isEmpty(parameterMap)) {
return params;
}
Map<String, Object> strMap = new HashMap<>();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String key = entry.getKey();
String[] value = entry.getValue();
Object strValue = "";
if (null != value && value.length > 0) {
strValue = value[0];
}
strMap.put(key, strValue);
}
params = JSON.toJSONString(sortMapByKey(strMap));
return params;
}
private static Map<String, Object> sortMapByKey(Map<String, Object> map) {
if (map == null || map.isEmpty()) {
return null;
}
Map<String, Object> sortMap = new TreeMap<String, Object>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return ((String) o1).compareTo((String) o2);
}
});
sortMap.putAll(map);
return sortMap;
}
}
加密方式保持一致:
前端引入js-sha256
import { sha256 } from 'js-sha256'
config.headers.Signature = sha256(sign)
后端:
String decryptData = DigestUtils.sha256Hex(newSignatureBuffer.toString());