【仅供内部供应商使用,不提供对外解答和培训】

Page tree

【仅供内部供应商使用,不提供对外解答和培训】

Skip to end of metadata
Go to start of metadata

一、特殊名词介绍

文件加密导出/导入:主要是指因客户安全管理要求或其他对接系统要求,文件的传输或者存储必须以加密的形式的场景。

二、常用接口说明

文件导入导出加密根据实践又细分为以下几种细分场景:

1.部分导出加密、导入解密 。部分的划分条件又分为:

A.根据用户(身份、权限)确定是否要加密:较常见的就是管理角色导出加密(文件必须用专门的工具才能打开),普通员工导出不加密

B.根据文件类型确定是否要加密:场景的 导出excel、word加密。pdf不加密

C.根据功能场景确定是否要加密:部分特殊模板导出加密其他不加密、(不)预览导出不加密,邮件推送加密 等等的

2.全部文件导出加密,导入解密


产品中设计的导出导入场景主要又包括以下的场景:

1.报表填报预览导入excel

2.文件控件上传文件

3.报表(不)预览导出

4.邮件(或其他推送)报表附件


技术方案:

产品的标准接口中,并未直接对文件的加解密提供专门的接口。实际解决时,开发者应根据具体的场景选择合适的切入点接口,切入具体的导入导出逻辑中对其进行干预。

就文件加解密来说,可选的切入点接口非常多。在本文中将选择其中三个切入点方案对导入导出加解密进行说明。

先看一下切入点的选择方法:

1.切入点接口需要时一个插件接口(这样比非插件接口作为切入点的方案稳定性就好一些)

2.因为涉及文件的输出,则一定最终都会通过输出流(OutputStream),选择的切入点需要能够篡改最终输出流中的数据

3.切入点接口一定要能对导出或导入的过程进行干预

通过这三个特征,经验丰富的开发者应该很容易找到非常多的切入点接口,对于新手开发者来说,就需要花时间多阅读插件的开发接口介绍文档了。


接下来看,假设通过某个切入点接口已经拿到了OutputStream,如何实现对数据流进行篡改。主要有以下几类方法:

1.通过类似wrapper类包裹原始的输出流,在包裹的输出流中进行数据的写操作(相对简单)

2.通过各种代理手段或字节码操作手段,实现对原本的输出流的替换或方法的改写(实现稍微麻烦一点)。

下面是个简单的示例

EncryptOutputStream.java
package com.tptj.demo.hg.export.encrypt;

import com.fr.intelli.record.Focus;
import com.fr.record.analyzer.EnableMetrics;
import com.fr.third.org.bouncycastle.util.Arrays;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
 * @author 秃破天际
 * @version 10.0
 * Created by 秃破天际 on 2021/7/15
 **/
@EnableMetrics
public class EncryptOutputStream extends ServletOutputStream {

    private OutputStream original = null;
    private boolean closed = false;
    private ByteArrayOutputStream crypt = new ByteArrayOutputStream();

    public EncryptOutputStream( OutputStream original ){
        this.original = original;
    }

    /**
     * 实际加密的代码就在这里实现了
     * @param bytes
     * @return
     */
    @Focus(id="com.tptj.demo.hg.export.encrypt.v10",text="Export Encrypt")
    private byte[] encrypt(byte[] bytes){
        //假设逆序就是加密了
        return Arrays.reverse(bytes);
    }

    @Override
    public void write(int b) throws IOException {
        if (closed) {
            throw new IOException("Cannot write to a closed output stream");
        }
        crypt.write(b);
    }

    @Override
    public void close() throws IOException {
        if( !closed ){
            byte[] bytes = crypt.toByteArray();
            bytes = encrypt(bytes);
            original.write(bytes);
            original.flush();
            original.close();
            closed = true;
        }
    }

    @Override
    public void flush() throws IOException {
        crypt.flush();
    }

    @Override
    public void write(byte[] data) throws IOException {
        write(data, 0, data.length);
    }

    @Override
    public void write(byte[] data, int off, int len) throws IOException {
        if (closed) {
            throw new IOException("Cannot write to a closed output stream");
        }
        crypt.write(data, off, len);
    }

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

    @Override
    public void setWriteListener(WriteListener listener) {

    }

}


方案一:以com.fr.report.fun.ExportOperateProvider作为切入点接口,实现对cpt报表部分文件类型导出进行加密

通过对(1)、导出接口关系和运用详解专题文档对整个导出过程的了解,可以知道,所有的结果报表文件导出(非文件控件的附件)最终都是通过Exporter对象进行导出的,该导出接口的入参都包含OutputStream对象,则我们可以利用一个类似ExporterWrapper的类来包裹原始的Exporter,在导出方法执行时,再利用OutputStreamWrapper包裹原来的输出流,从而实现对输出数据的加密

如果要对多种文件类型都生效,我们就需要实现多个该接口对不同的类型都进行相同的篡改。

EncryptExporter.java
package com.tptj.demo.hg.export.encrypt.demo1;

import com.fr.io.exporter.AppExporter;
import com.fr.main.workbook.ResultWorkBook;
import com.fr.page.PageSetCreator;
import com.fr.page.PageSetProvider;
import com.fr.web.core.ReportRepositoryDeal;
import com.tptj.demo.hg.export.encrypt.EncryptOutputStream;

import java.io.OutputStream;

/**
 * @author 秃破天际
 * @version 10.0
 * Created by 秃破天际 on 2021/7/15
 **/
public class EncryptExporter implements AppExporter {

    private AppExporter original;

    public EncryptExporter(AppExporter original){
        this.original = original;
    }
    @Override
    public void export( OutputStream out, ResultWorkBook book ) throws Exception {
        original.export( new EncryptOutputStream(out), book );
    }

    @Override
    public void export( OutputStream out, PageSetProvider pageSet ) throws Exception {
        original.export( new EncryptOutputStream(out), pageSet );
    }

    @Override
    public void export(OutputStream out, ResultWorkBook book, PageSetCreator creator, ReportRepositoryDeal repo, int[] sheets) throws Exception {
        original.export( new EncryptOutputStream(out), book,creator,repo,sheets);
    }

    @Override
    public void export(OutputStream out, ResultWorkBook book, PageSetProvider pageSet, ReportRepositoryDeal repo, int[] sheets) throws Exception {
        original.export( new EncryptOutputStream(out), book,pageSet,repo,sheets);
    }

    @Override
    public void setVersion(Object o) {
        original.setVersion(o);
    }
}

EncryptExcelOperate.java
package com.tptj.demo.hg.export.encrypt.demo1;

import com.fr.io.collection.ExportCollection;
import com.fr.io.exporter.AppExporter;
import com.fr.stable.web.SessionProvider;
import com.fr.web.core.reserve.ExcelOperate;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author 秃破天际
 * @version 10.0
 * Created by 秃破天际 on 2021/7/15
 **/
public class EncryptExcelOperate extends ExcelOperate {

    @Override
    public ExportCollection newExportCollection(HttpServletRequest req, HttpServletResponse res,
                                                SessionProvider sessionIDInfor, String filename ){
        ExportCollection collection = super.newExportCollection(req, res, sessionIDInfor, filename);
        AppExporter exporter = collection.getExporter();
        collection.setExporter( new EncryptExporter(exporter) );
        return collection;
    }
}



方案二:以com.fr.report.fun.ExportExtensionProcessor作为切入点接口,实现对cpt报表的所有文件类型导出进行加密

通过对(1)、导出接口关系和运用详解专题文档对整个导出过程的了解,可以知道,在基于HTTP请求发起的导出过程中ExportExtensionProcessor接口优先于ExportOperateProvider接口,且ExportExtensionProcessor对cpt导出的所有类型都生效。具体的实现方法与使用ExportOperateProvider作为切入点是一致的

Demo.java
package com.tptj.demo.hg.export.encrypt.demo2;

import com.fr.io.collection.ExportCollection;
import com.fr.io.exporter.AppExporter;
import com.fr.web.core.ReportSessionIDInfor;
import com.fr.web.core.reserve.DefaultExportExtension;
import com.tptj.demo.hg.export.encrypt.demo1.EncryptExporter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author 秃破天际
 * @version 10.0
 * Created by 秃破天际 on 2021/7/15
 **/
public class Demo extends DefaultExportExtension {

    @Override
    public ExportCollection createCollection(HttpServletRequest req, HttpServletResponse res,
                                             ReportSessionIDInfor info, String format, String filename, boolean isEmbed) throws Exception {
        ExportCollection collection = super.createCollection(req, res, info, format,filename,isEmbed);
        AppExporter exporter = collection.getExporter();
        collection.setExporter( new EncryptExporter(exporter) );
        return collection;
    }
}

方案三:以com.fr.decision.fun.GlobalRequestFilterProvider作为切入点接口,对所有HTTP请求发起的文件导入导出请求进行加解密(定时任务、邮件附件场景不适应)

这个方案相对容易理解,只要是由请求触发,文件最终在同一次请求对于的响应中要输出到前端,那么就肯定可以直接替换掉响应中的输出流。

同理,只要是从前端上传文件的操作,文件信息一定在请求的输入流中,同样可以通过wrapper实现对输入流的替换

从而实现对于的加解密

DecryptRequest.java
package com.tptj.demo.hg.export.encrypt.demo3;

import com.fr.general.IOUtils;
import com.fr.third.org.bouncycastle.util.Arrays;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * @author 秃破天际
 * @version 10.0
 * Created by 秃破天际 on 2021/7/15
 **/
public class DecryptRequest extends HttpServletRequestWrapper {
    private byte[] data;
    public DecryptRequest(HttpServletRequest request) {
        super(request);
        init();
    }

    private void init(){
        try{
            data = IOUtils.inputStream2Bytes(getRequest().getInputStream());
            //实现解密(假设加密就是逆序,那么解密就是再次逆序即可)
            data = Arrays.reverse(data);
        }catch(Exception e){
            data = new byte[0];
        }
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        try{
            return new DelegatingServletInputStream(new ByteArrayInputStream( data ));
        }catch (Exception e){
            return null;
        }
    }

    @Override
    public BufferedReader getReader()throws IOException{
        return new BufferedReader(new InputStreamReader(new ByteArrayInputStream( data )));
    }
}
EncryptResponse.java
package com.tptj.demo.hg.export.encrypt.demo3;

import com.tptj.demo.hg.export.encrypt.EncryptOutputStream;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;

/**
 * @author 秃破天际
 * @version 10.0
 * Created by 秃破天际 on 2021/7/15
 **/
public class EncryptResponse extends HttpServletResponseWrapper {

    public EncryptResponse(HttpServletResponse response) {
        super(response);
    }

    public ServletOutputStream getOutputStream()throws IOException {
        ServletOutputStream original =  super.getOutputStream();
        return new EncryptOutputStream(original);
    }

}


文件加解密的方法对调研评估的影响:

如果是对接第三方加密系统的,加密系统会提供具体的SDK或加解密接口。如果有对应的接口文档和示例demo的,相对会比较简单。对接第三方加密,最难的就是只有一个自研发的加密方法名称或系统名称,但是缺乏文档和示例demo,需要人工对接交流的(一般常见于最终客户早期找了其他厂商开发的加解密系统,由于交接问题导致最终材料交付不全遗失等情况),这类情况就工作量开销就非常大了,开发者在评估的时候需要慎重考虑。对接第三方加密系统的,评估时还要考虑测试成本和职责划分,即用户是否可以提供对应的测试环境给开发者;是否要求必须驻场才能测试;开发者可以做的自测能到什么程度;开发是否是盲写无法调试;这些问题都影响到最终的工作量和工期的评估,开发者需要提前做好风险控制。

如果对接的是业内常见的标准加密方法:诸如 3DES\AES 这类的算法,则是否有文档和示例对评估和开发测试工作影响基本不大。这类对接也是最简单的。开发者调研的中心应在用户的使用场景和交互习惯上。


三、开源案例

免责声明:所有文档中的开源示例,均为开发者自行开发并提供。仅用于参考和学习使用,开发者和官方均无义务对开源案例所涉及的所有成果进行教学和指导。若作为商用一切后果责任由使用者自行承担。

demo-export-encrypt【示例方案的完整代码】

  • No labels