背景
在一次项目等保测评中,扫描出的问题有一项为:传输过程中未加密。为了使数据更加安全,决定将项目的请求参数和响应,全部采用对称加密之后传输,需要方根据指定的秘钥进行解密。
项目简介
项目基于 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;
}
}