背景
在一次项目等保测评中,扫描出的问题有一项为:传输过程中未加密。为了使数据更加安全,决定将项目的请求参数和响应,全部采用对称加密之后传输,需要方根据指定的秘钥进行解密。
项目简介
项目基于 SpringBoot 2.5.x 进行构建。由于一些原因,项目中使用 Get、Post(form、json)方式请求。
方案
加密传输统一字段: payload 。字段值为,原有数据 json 化加密之后的字符串。对于地址栏参数不加密,文件上传接口也不加密。
解密参数
- 客户端端将所有的参数 json 化之后加密。
- 服务端将参数解密之后使用
form 表单形式
- 采用 Filter 的方式进行参数解析
// org.springframework.web.filter.OncePerRequestFilter @Component public class EncryptionRequestFilter extends OncePerRequestFilter { @Value("${encryption.mark:true}") private Boolean encryptionMark; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (encryptionMark) { filterChain.doFilter(new EncryptionHttpRequestWrapper(new CachedRequestWrapper(request)), response); } else { filterChain.doFilter(request, response); } } } // 缓存 request 中的输入流 InputStream,因为这个使用过一次之后就会为空,后续不方便 public class CachedRequestWrapper extends HttpServletRequestWrapper { private byte[] requestBodyByte; public CachedRequestWrapper(HttpServletRequest request) { super(request); try { this.requestBodyByte = StreamUtils.copyToByteArray(request.getInputStream()); } catch (IOException e) { // capture but do nothing this.requestBodyByte = new byte[0]; } } @Override public ServletInputStream getInputStream() { ByteArrayInputStream bais = new ByteArrayInputStream(requestBodyByte); return new ServletInputStream() { @Override public boolean isFinished() { return bais.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener listener) { } @Override public int read() throws IOException { return bais.read(); } }; } public byte[] getRequestBodyByte() { return requestBodyByte; } }
- 参数解密,EncryptionHttpRequestWrapper 这种能够解决简单的一些参数,对与复杂形式结构的数据就无能为力
// 无法解决的结构: /** "attachList[0].attachName": "2024-11-14 11:46:55-2.jpg", "attachList[0].attachAddress": "/upload/jpg/20241114/367fe4ea04ed41fd92975b93e7abd722.jpg", "attachList[0].attachSuffix": "/jpg", "attachList[0].attachSize": "12709" 这种结构使用 json 的方式是很好解决的,但是这里无法去改。引入另一种 org.springframework.web.method.support.HandlerMethodArgumentResolver 在执行 controller 方法之前进行参数解析。 */ // 解密方式一 // 解密 request 包装类 public class EncryptionHttpRequestWrapper extends HttpServletRequestWrapper { private Map<String, String[]> paramsMap = null; private static final JSONConfig CREATE = JSONConfig.create().setDateFormat(DateUtil.YYYY_MM_DD_HH_MM_SS); private final HttpServletRequest request; public EncryptionHttpRequestWrapper(CachedRequestWrapper httpServletRequest) { super(httpServletRequest); this.request = httpServletRequest; String encryption = request.getParameter("payload"); if (StrUtil.isBlank(encryption)) { byte[] requestBodyByte = httpServletRequest.getRequestBodyByte(); String requestBodyString = URLDecoder.decode(StrUtil.str(requestBodyByte, StandardCharsets.UTF_8)); if (StrUtil.isBlank(requestBodyString)) { return; } List<String> paramSplit = StrUtil.split(requestBodyString, "&"); for (String param : paramSplit) { if (StrUtil.isNotBlank(param) && param.startsWith("payload")) { encryption = param.substring(8); break; } } } if (StrUtil.isBlank(encryption)) { return; } String decrypt = AESUtil.decryptionLoginInfo(encryption); JSONObject decryptParamMap = JSONUtil.parseObj(decrypt, CREATE); this.paramsMap = new HashMap<>(); this.paramsMap.put("payload", new String[]{encryption}); Parameters parameters = new Parameters(); convert(decryptParamMap, "", parameters); Enumeration<String> parameterNames = parameters.getParameterNames(); while (parameterNames.hasMoreElements()) { String parameterName = parameterNames.nextElement(); this.paramsMap.put(parameterName, parameters.getParameterValues(parameterName)); } } @Override public String getParameter(String name) { if (CollectionUtil.isEmpty(paramsMap)) { return request.getParameter(name); } return Optional.ofNullable(paramsMap.get(name)).map(e -> e[0]).orElse(null); } @Override public Map<String, String[]> getParameterMap() { if (CollectionUtil.isEmpty(paramsMap)) { return request.getParameterMap(); } return paramsMap; } @Override public Enumeration<String> getParameterNames() { if (CollectionUtil.isEmpty(paramsMap)) { return request.getParameterNames(); } Queue<String> queue = new LinkedBlockingQueue<>(paramsMap.keySet()); return new Enumeration<String>() { @Override public boolean hasMoreElements() { return !queue.isEmpty(); } @Override public String nextElement() { return queue.poll(); } }; } @Override public String[] getParameterValues(String name) { if (CollectionUtil.isEmpty(paramsMap)) { return request.getParameterValues(name); } return paramsMap.get(name); } public void convert(JSONObject jsonObject, String preKey, Parameters parameters) { for (Map.Entry<String, Object> entry : jsonObject.entrySet()) { String key; if (StrUtil.isBlank(preKey)) { key = entry.getKey(); } else { key = preKey + "." + entry.getKey(); } Object value = jsonObject.get(entry.getKey()); if (Objects.isNull(value)) { continue; } boolean primitive = value.getClass().isPrimitive(); if (primitive) { parameters.addParameter(key, String.valueOf(value)); } else if (value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long || value instanceof Float || value instanceof Double || value instanceof Character || value instanceof Boolean) { parameters.addParameter(key, String.valueOf(value)); } else if (value instanceof JSONObject) { convert((JSONObject) value, key, parameters); } else if (value instanceof JSONArray) { JSONArray array = (JSONArray) value; for (int i = 0; i < array.size(); i++) { Object obj = array.getObj(i); if (Objects.isNull(obj)) { continue; } if (obj instanceof Byte || obj instanceof Short || obj instanceof Integer || obj instanceof Long || obj instanceof Float || obj instanceof Double || obj instanceof Character || obj instanceof Boolean || obj.getClass().isPrimitive()) { parameters.addParameter(key + "[" + i + "]", String.valueOf(obj)); } else if (obj instanceof JSONObject) { JSONObject el = array.getJSONObject(i); convert(el, key + "[" + i + "]", parameters); } else if (obj instanceof String) { parameters.addParameter(key + "[" + i + "]", String.valueOf(obj)); } } } else { parameters.addParameter(key, value.toString()); } } } } // 解密方式二 @Configuration public class ProjectWebConfigurer implements WebMvcConfigurer { @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(new CustomMapArgumentResolver()); } } public class CustomMapArgumentResolver implements HandlerMethodArgumentResolver { // 判断是否要启用该参数解析实现,这里只对复杂的接口进行解析 @Override public boolean supportsParameter(MethodParameter parameter) { Method method = parameter.getMethod(); if (Objects.isNull(method)) { return false; } Parameter[] parameters = method.getParameters(); if (parameters.length > 1) { return false; } Parameter realParameter = parameters[0]; Class<?> type = realParameter.getType(); boolean primitive = type.isPrimitive(); if (primitive) { return false; } else if (Byte.class.getName().equals(type.getName()) || Short.class.getName().equals(type.getName()) || Integer.class.getName().equals(type.getName()) || Long.class.getName().equals(type.getName()) || Float.class.getName().equals(type.getName()) || Double.class.getName().equals(type.getName()) || Character.class.getName().equals(type.getName()) || String.class.getName().equals(type.getName()) || Boolean.class.getName().equals(type.getName())) { return false; } return true; } // 解析方案 读取到对应的数据流,然后 将 attachList[0].attachSize 形式的参数,转换为规范的 json 格式。这里偷个懒,只转换了一层,实际项目中也只有一层。 @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavc, NativeWebRequest request, WebDataBinderFactory binderFactory) throws Exception { String payload = request.getParameter("payload"); Class<?> parameterType = parameter.getParameterType(); if (StrUtil.isBlank(payload)) { return BeanUtils.instantiateClass(parameterType); } JSONObject jsonObject = JSON.parseObject(AESUtil.decryptionLoginInfo(payload)); formatParamForJsonObj(jsonObject); Object obj = jsonObject.toJavaObject(parameterType); PropertyDescriptor payloadPd = BeanUtils.getPropertyDescriptor(parameterType, "payload"); if (Objects.nonNull(payloadPd)) { payloadPd.getWriteMethod().invoke(obj, payload); } return obj; } // 规范 json ,确保复杂结构形式的参数能正常被转化为实体 private void formatParamForJsonObj(JSONObject json) { Set<String> needFormatParamNameSet = new HashSet<>(); Set<String> needFormatParamNameDotSet = new HashSet<>(); for (String key : json.keySet()) { if (key.contains("[") && key.contains("]")) { needFormatParamNameSet.add(key); } else if (key.contains(".")) { needFormatParamNameDotSet.add(key); } } for (String paramName : needFormatParamNameSet) { int i = paramName.indexOf("["); int j = paramName.indexOf("]"); String startPropertyName = paramName.substring(0, i); JSONArray propertyValue = json.getJSONArray(startPropertyName); if (Objects.isNull(propertyValue)) { propertyValue = new JSONArray(); json.put(startPropertyName, propertyValue); } if (paramName.contains(".")) { int elIndex = Integer.parseInt(paramName.substring(i + 1, j)); while (elIndex >= propertyValue.size()) { propertyValue.add(new JSONObject()); } JSONObject propertyElValue = propertyValue.getJSONObject(elIndex); propertyElValue.put(paramName.substring(paramName.indexOf(".") + 1), json.get(paramName)); } else { propertyValue.set(Integer.parseInt(paramName.substring(i + 1, j)), json.get(paramName)); } } for (String paramName : needFormatParamNameDotSet) { int i = paramName.indexOf("."); String startPropertyName = paramName.substring(0, i); JSONObject propertyValue = json.getJSONObject(startPropertyName); if (Objects.isNull(propertyValue)) { propertyValue = new JSONObject(); json.put(startPropertyName, propertyValue); } propertyValue.put(paramName.substring(i + 1), json.get(paramName)); } } }
json 形式
使用 org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter 处理 所有被 @RequestBody 注解标注的方法参数。
@RestControllerAdvice @Component public class EncryptionRequestBodyAdvice extends RequestBodyAdviceAdapter { @Value("${encryption.mark:true}") private Boolean encryptionMark; @Override public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { return true; } @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { if (!encryptionMark) { return inputMessage; } return new EncryptionHttpInputMessageWrapper(inputMessage); } } public class EncryptionHttpInputMessageWrapper implements HttpInputMessage { private final HttpInputMessage httpInputMessage; public EncryptionHttpInputMessageWrapper(HttpInputMessage httpInputMessage) { this.httpInputMessage = httpInputMessage; } @Override public InputStream getBody() throws IOException { String dataStr = null; String decrypt; try { // 提取数据 InputStream is = httpInputMessage.getBody(); byte[] data = StreamUtils.copyToByteArray(is); dataStr = new String(data, StandardCharsets.UTF_8); JSONObject jsonObject = JSONUtil.parseObj(dataStr); decrypt = AESUtil.decryptionLoginInfo(jsonObject.getStr("payload")); } catch (Exception e) { if (dataStr == null) { dataStr = ""; } return new ByteArrayInputStream(dataStr.getBytes(StandardCharsets.UTF_8)); } return new ByteArrayInputStream(decrypt.getBytes(StandardCharsets.UTF_8)); } @Override public HttpHeaders getHeaders() { return httpInputMessage.getHeaders(); } }
加密响应
org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice 对所用 json 形式的响应加密。非json形式的不做处理
@RestControllerAdvice @Component public class EncryptionResponseBodyAdvice implements ResponseBodyAdvice<Object> { @Resource private ObjectMapper jacksonObjectMapper; @Value("${encryption.mark:true}") private Boolean encryptionMark; @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (!encryptionMark) { return body; } if (MediaType.APPLICATION_JSON.equals(selectedContentType) || MediaType.APPLICATION_JSON_UTF8.equals(selectedContentType)) { String encryptHex; try { encryptHex = AESUtil.encryptionLoginInfo(jacksonObjectMapper.writeValueAsString(body)); } catch (JsonProcessingException e) { encryptHex = ""; } return Collections.singletonMap("payload", encryptHex); } return body; } }