SpringBoot前后端接口加解密--解决方案

开放接口

- 通信方式采用HTTP+JSON或消息中间件进行通信。
- 调用接口之前需要使用登录鉴权接口获得token。
- 当鉴权成功之后才能调用其他接口(携带Token)。
登录接口:
Code	说明
200	成功
401	未授权,请先登录。
403	没有访问权限
404	接口不存在
500	接口内部错误

但是开放接口报文密文篡改问题

传入报文加密 :但系统已经很臃肿--很多的业务接口了,不可能每一个接口都去实现一遍报文解密;所以抽离到通用服务上去,然后业务接口无感实现

所以在网关层处理就最合适了

代码示例---基于Filter实现报文解密,然后转给业务接口

/**
 * @author Administrator
 * @apiNote 移动设备传输数据解密
 * @date 2024/9/4 11:23
 */
@Component
public class MobileDevicesReqDataDecryptFilter implements Filter, InitializingBean {
    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 是否开启解密
     */
    @Value("${cztech.manage.gateway.mobile-devices.enable-decrypt:true}")
    private boolean enable;

    @Value("${cztech.manage.gateway.mobile-devices-iv:56542a855c2756e9}")
    private String aesIV;
    @Value("${cztech.manage.gateway.mobile-devices-encrypt-key:C9JUQeuOmEu5ZvJKprEMuA==}")
    private String aesKey;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    /**
     * application/json
     * application/x-www-form-urlencoded。
     * 移动设备的 前端和后端只针对Content-Type 为上述格式数据进行加解密。
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String contentType = request.getContentType();
        // app原生请求数据时需要在http header中添加operateSystem(取值为1表示ANDROID,2-表示IOS)
        if (handlerDecryptRequest(servletRequest, servletResponse, filterChain, response, request, contentType)) {
            stopWatch.stop();
            if (logger.isDebugEnabled()) {
                logger.debug("MobileDevicesReqDataDecryptFilter cost time: {} ms", stopWatch.getTotalTimeMillis());
            }
            return;
        }
        stopWatch.stop();
        if (logger.isDebugEnabled()) {
            logger.debug("MobileDevicesReqDataDecryptFilter cost time: {} ms", stopWatch.getTotalTimeMillis());
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

    public boolean handlerDecryptRequest(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain, HttpServletResponse response, HttpServletRequest request, String contentType) throws IOException {
        // 判断开关是否开启
        if (!enable) {
            return false;
        }
        if(StringUtils.isEmpty(contentType)){  // content-type 为空则不解密
            return false;
        }
        // 如果开启解密开关就走以下逻辑
        if (request.getMethod().equalsIgnoreCase("POST") &&
                (contentType.contains("application/json")
                || contentType.contains("application/x-www-form-urlencoded"))) {
            String operateSystem = request.getHeader("operateSystem");
            if (StringUtils.isNotEmpty(operateSystem) && ("1".equals(operateSystem) || "2".equals(operateSystem))) {
                ServletInputStream inputStream = servletRequest.getInputStream();
                String originBodyStr = null;
                try {
                    originBodyStr = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
                    if (StringUtils.isEmpty(originBodyStr)) {
                        filterChain.doFilter(new MobileDevicesReqServletRequestWrapper((HttpServletRequest) servletRequest, originBodyStr), servletResponse);
                        return true;
                    }
                    //解密数据
                    String decryptData = decryptData(originBodyStr);
                    logger.debug("decryptData is :" + decryptData);
                    filterChain.doFilter(new MobileDevicesReqServletRequestWrapper((HttpServletRequest) servletRequest, decryptData), servletResponse);
                } catch (Exception e) {
                    response.sendError(HttpStatus.BAD_REQUEST.value(), "非法请求");
                }
                return true;
            }
        }
        return false;
    }

    /**
     * 请求参数进行解密(加密算法为AES,模式为CBC,填充方式为PKCS7)
     *
     * @param originBodyStr originBodyStr
     * @return String
     */
    public String decryptData(String originBodyStr) {
        AES aes = new AES("CBC", "PKCS7Padding",
                aesKey.getBytes(StandardCharsets.UTF_8),
                aesIV.getBytes(StandardCharsets.UTF_8));
        return aes.decryptStr(originBodyStr);
    }

    @Override
    public void destroy() {
        logger.info("MobileDevicesReqDataDecryptFilter destroy");
        Filter.super.destroy();
    }

    @Override
    public void afterPropertiesSet() throws Exception {

    }


    /**
     * http request包装器。
     * 数据解密之后 body 已经发生变化,所以重写{@link #getInputStream()} 业务接口才能获取解密后的数据。
     * 包装器只针对 content-type为json格式(其他格式可能会有大数据导致能溢出)。
     * 为了使spring 自动解析参数所以需要重写{@link #getParameterNames()} 和{@link #getParameterValues(String)}
     */
    class MobileDevicesReqServletRequestWrapper extends HttpServletRequestWrapper {

        private String body;

        private HttpServletRequest request;
        /**
         * 参数是否已经解析,true-解析,false-未解析
         */
        private boolean parseParam;
        /**
         * 请求参数
         */
        private Map<String, List<String>> parameters = new HashMap<>();

        public MobileDevicesReqServletRequestWrapper(HttpServletRequest request, String body) {
            super(request);
            this.body = body;
            this.request = request;
        }

        @Override
        public Map<String, String[]> getParameterMap() {
            if (!parseParam){
                parseParameter(request.getContentType());
            }
            HashMap<String,String[]> parametersArray = new HashMap<>();
            parameters.forEach((key,value)->{
                String[] data =new String[value.size()];
                value.toArray(data);
                parametersArray.put(key,data);

            });
            return parametersArray;
//            return super.getParameterMap();
        }

        @Override
        public Enumeration<String> getParameterNames() {
            // 判断 content-type 是否为 form_urlEncoded; ,如果是再解析参数
            if (parseParam) {
                return Collections.enumeration(parameters.keySet());
            }
            String contentType = request.getContentType();
            if (StringUtils.isNotEmpty(contentType) && StringUtils.isNotEmpty(body)) {
                parseParameter(contentType);
            }
            parseParam = true;
            return Collections.enumeration(parameters.keySet());
        }

        @Override
        public int getContentLength() {
            return (int) getContentLengthLong();
        }

        @Override
        public long getContentLengthLong() {
            return body.getBytes().length;
        }

        private void parseParameter(String contentType) {
            if (contentType.equals(ContentType.FORM_URLENCODED.getValue())) { //
                String[] bodyParameters = body.split("&");
                for (String bodyParameter : bodyParameters) {
                    if (StringUtils.isNotEmpty(bodyParameter)) {
                        String[] parameter = bodyParameter.split("="); //解析键值对
                        if (parameter.length == 2) {
                            String name = parameter[0];
                            String value = parameter[1];
                            if (StringUtils.isNotEmpty(name)) {
                                List<String> values = parameters.get(name);
                                if (values == null) {
                                    values = new ArrayList<>();
                                    parameters.put(name, values);
                                }
                                if (StringUtils.isNotEmpty(value)) {
                                    //参数值需要使用urlDecode 。
                                    values.add(URLDecoder.decode(value, Charset.forName("UTF-8")));
                                }
                            }
                        }
                    }
                }
            }
        }

        @Override
        public String[] getParameterValues(String name) {
            if (StringUtils.isEmpty(name)) {
                return null;
            }
            getParameterNames();
            List<String> values = parameters.get(name);
            if (ListUtil.isBlank(values)) { // 参数值判断
                return null;
            }
            String[] valueArray = new String[]{};
            return values.toArray(valueArray);
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            ByteArrayInputStream inputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
            return new ServletInputStream() {

                @Override
                public int read() throws IOException {
                    return inputStream.read();
                }

                @Override
                public boolean isFinished() {
                    return false;
                }

                @Override
                public boolean isReady() {
                    return true;
                }

                @Override
                public void setReadListener(ReadListener readListener) {

                }
            };
        }
    }
}

核心就是从Request拿到加密报文:在doFilter方法里解密

但是Request的getInputStream流后就不可再用了,整个链路的Request IO 传给下一场时,下一层无法使用

解决方案就是:new 一个自定义的 HttpServletRequestWrapper 替代原来的 Request 传给过Filter链下一层使用

posted on 2024-09-23 10:20  白嫖老郭  阅读(539)  评论(0编辑  收藏  举报

导航