SpringBoot对于传输过程中的加解密方案

doMore 71 2024-11-14

背景

在一次项目等保测评中,扫描出的问题有一项为:传输过程中未加密。为了使数据更加安全,决定将项目的请求参数和响应,全部采用对称加密之后传输,需要方根据指定的秘钥进行解密。

项目简介

项目基于 SpringBoot 2.5.x 进行构建。由于一些原因,项目中使用 Get、Post(form、json)方式请求。

方案

加密传输统一字段: payload 。字段值为,原有数据 json 化加密之后的字符串。对于地址栏参数不加密,文件上传接口也不加密。

解密参数

  1. 客户端端将所有的参数 json 化之后加密。
  2. 服务端将参数解密之后使用

form 表单形式

  1. 采用 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;  
    }  
}

  1. 参数解密,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;  
    }  
}