首页 > 其他分享 >SpringBoot前后端接口加解密--解决方案

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

时间:2024-09-23 10:27:04浏览次数:9  
标签:return SpringBoot -- request 加解密 Override new public String

开放接口

- 通信方式采用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链下一层使用

标签:return,SpringBoot,--,request,加解密,Override,new,public,String
From: https://www.cnblogs.com/gtnotgod/p/18426513

相关文章

  • 免费开源的 PDF 工具箱,功能丰富,操作简单,获赞 42k+!
    ​PDF是工作中常见的文件格式,经常需要对其进行合并、分割、旋转、加水印等操作。而目前市面上大部分PDF软件,免费版只能用来看PDF,修改PDF的功能都是付费的。今天就和大家分享一个本地化处理PDF的神器:Stirling-PDF,它功能强大,几乎可以满足任何操作,而且是本地运行,不用担心数......
  • [Python手撕]判断回文链表
    #Definitionforsingly-linkedlist.#classListNode:#def__init__(self,val=0,next=None):#self.val=val#self.next=nextclassSolution:defisPalindrome(self,head:Optional[ListNode])->bool:deffindmid(he......
  • SpringBoot + Disruptor 实现特快高并发处理,支撑每秒 600 万订单无压力!
    01、背景02、Disruptor介绍03、Disruptor的核心概念04、RingBuffer05、SequenceDisruptor06、Sequencer07、SequenceBarrier08、WaitStrategy09、Event10、EventProcessor11、EventHandler12、Producer13、案例-demo14、总结01、背景工作中遇到项目使用Di......
  • markdown 转 ppt
    marpit转html如果不需要插件,在vsCode内安装marpforvscode即可Ctrl+Shift+P或F1,搜索Marp:ExportSlideDeck如果需要自己安装一些插件markdown-it-*,需要使用下面的.jsimportfsfrom'fs'import{log}from'console'importMarpitfrom'@marp-team/marpit'impor......
  • SpringBoot实战:JWT Token 自动续期的解决方案
    前言在前后端分离的开发架构中,当用户成功登录后,后端服务会生成一个JWT(JSONWebTokens)token,并将其返回给前端。前端(如Vue应用)接收到此token后,通常会将其存储在LocalStorage中以方便后续请求时使用。每次向后端发送请求时,前端会将这个token作为请求头的一部分发送给后端,以便后端通......
  • Spring Boot集成OpenPDF实现PDF导出功能
    如果你想要在SpringBoot项目中使用OpenPDF来生成PDF文件,而不是iText,你可以通过将HTML转换成PDF的方式来实现。OpenPDF是一个开源的JavaPDF库,它基于iText5.x版本,但是它主要提供了HTML到PDF的转换能力。下面是如何在SpringBoot项目中设置并使用OpenPDF来生成PDF文件的一个简单......
  • Microsoft Edge 五个好用的插件
    ......
  • P1043 [NOIP2003 普及组] 数字游戏
    又是一个思维毒瘤好题,但dp题都是这样,菜就多练吧。拆环为链,前缀和加速计算,枚举断点数、起点、终点、断点。#include<bits/stdc++.h>usingnamespacestd;#definelllonglongconstintN=105;inta[N*2];intn,m;intsum[N*2];intmx[N][N][N];intmi[N][N][N];int......
  • Spring Boot入门
    SpringBoot是一个用于简化Spring应用程序开发的框架,它提供了快速构建、开箱即用的特性,使得开发人员可以更快速地搭建和部署应用程序。SpringBoot的核心理念是“约定优于配置”,它通过自动化配置和预定义的依赖项,减少了开发人员在项目配置上的工作量。入门SpringBoot的......
  • 用了MyBatis的项目 如何优雅地打印SQL
    前言在使用MyBatis或者MyBatis-Plus作为ORM框架的时候,会发现默认的日志输出是下面这样的:在参数少并且SQL简单的情况下,这样的SQL我们能通过手动去替换占位符,来获取到真正执行的SQL。但是如果是比较复杂的SQL,或者查询参数比较多的话,一个个替换就比较费时费力了。MyBatisPlugin......