注:本文所涉及的相关技术和工具包,由于具有超强的入侵能力,禁止用于商城插件。以免大面积使用带来的不稳定风险!仅能在定制任务中取得负责人许可的情况下使用!
在学习了TemplateEncryptProvider接口之后,开发者可以对所有的模板进行统一的加解密。但如果需要根据模板的不同设置或者是根据模板的路径等等其他信息来确定是否加密,以及加密的策略。则单一的TemplateEncryptProvider难以满足需要。
本文以一个具体场景为例,说明复杂场景的模板加解密的实现基本过程。
场景:区别于当前产品的模板统一密钥加解密,实现每个模板单独设置加密密钥。用户编辑加密的模板前需要先验证改模板的密钥,合法后才能编辑。
首先利用动态字节码工具包,在模板的保存和编辑触发逻辑中注入一个临时的切面。以便传递相关的环境信息和插入UI交互逻辑
package com.tptj.demo.hg.template.encrypt; import com.fr.base.io.EncryptIOFileProxy; import com.fr.base.io.IOFile; import com.fr.file.FILE; import com.fr.invoke.Reflect; import com.fr.log.FineLoggerFactory; import com.tptj.demo.hg.template.encrypt.ui.DecodeDialog; import com.tptj.demo.hg.template.encrypt.ui.SpEncryptAttrMark; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021/9/12 **/ public class BaseInterceptor { /** * 在打开模板前对加密的模板插入输入密码的弹窗 * @param file */ public static void intercept4Edit(FILE file){ try { file.asInputStream(); if( !DemoEncryptor.hasSecret() ){ return; } String secret = DemoEncryptor.getSecret(); secret = new DecodeDialog(file,secret).getSecret(); DemoEncryptor.setSecret(secret); } catch (Exception e) { FineLoggerFactory.getLogger().error(e,e.getMessage()); } } /** * 在保存模板前获取配置的模板密钥,以便加密接口进行加密 * @param origin */ public static void intercept4Save( EncryptIOFileProxy origin ){ IOFile report = Reflect.on(origin).get("file"); SpEncryptAttrMark mark = report.getAttrMark(SpEncryptAttrMark.XML_TAG); if( null != mark ){ DemoEncryptor.setSecret(mark.getSecret()); } } } |
在产品中注入3个切面,以便注入交互和相关逻辑
package com.fr.design.mainframe.app; import com.fr.file.FILE; import com.fr.main.impl.WorkBook; import com.tptj.demo.hg.template.encrypt.BaseInterceptor; import com.tptj.tool.hg.dynamic.agent.source.AccessPoint; import com.tptj.tool.hg.dynamic.agent.source.Context; import com.tptj.tool.hg.dynamic.agent.source.Source; import com.tptj.tool.hg.dynamic.agent.version.ModuleReport; import com.tptj.tool.hg.dynamic.agent.version.VersionChecker; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021/9/11 * 注入CptApp#asIOFile 的切面,在打开加密cpt时,注入密码输入UI交互 **/ @VersionChecker(value = 10020210101l, loader = ModuleReport.class) @Source(CptApp.class) public class SpCptApp extends Context { @AccessPoint public WorkBook asIOFile(FILE file, boolean needCheck) { BaseInterceptor.intercept4Edit(file); return null; } } |
package com.fr.design.mainframe.app; import com.fr.file.FILE; import com.fr.form.main.Form; import com.tptj.demo.hg.template.encrypt.BaseInterceptor; import com.tptj.tool.hg.dynamic.agent.source.AccessPoint; import com.tptj.tool.hg.dynamic.agent.source.Context; import com.tptj.tool.hg.dynamic.agent.source.Source; import com.tptj.tool.hg.dynamic.agent.version.ModuleReport; import com.tptj.tool.hg.dynamic.agent.version.VersionChecker; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021/9/12 * 注入FormApp#asIOFile 的切面,在打开加密form时,注入密码输入UI交互 **/ @VersionChecker(value = 10020210101l, loader = ModuleReport.class) @Source(FormApp.class) public class SpFormApp extends Context { @AccessPoint public Form asIOFile(FILE file) { BaseInterceptor.intercept4Edit(file); return null; } } |
package com.fr.design.mainframe.app; import com.fr.base.io.EncryptIOFileProxy; import com.tptj.demo.hg.template.encrypt.BaseInterceptor; import com.tptj.tool.hg.dynamic.agent.source.AccessPoint; import com.tptj.tool.hg.dynamic.agent.source.Context; import com.tptj.tool.hg.dynamic.agent.source.Source; import com.tptj.tool.hg.dynamic.agent.version.ModuleReport; import com.tptj.tool.hg.dynamic.agent.version.VersionChecker; import java.io.OutputStream; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021/9/11 * 注入EncryptIOFileProxy#export的切面,在保存模板时,传递密钥信息,以便加密 **/ @VersionChecker(value = 10020210101l, loader = ModuleReport.class) @Source(EncryptIOFileProxy.class) public class SpEncryptIOFileProxy extends Context { @AccessPoint public boolean export(OutputStream out) throws Exception { BaseInterceptor.intercept4Save(getOrigin()); return false; } } |
接着使用插件的生命周期接口把切面注入生效
package com.tptj.demo.hg.template.encrypt; import com.fr.design.mainframe.app.SpCptApp; import com.fr.design.mainframe.app.SpEncryptIOFileProxy; import com.fr.design.mainframe.app.SpFormApp; import com.fr.intelli.record.Focus; import com.fr.plugin.context.PluginContext; import com.fr.plugin.observer.inner.AbstractPluginLifecycleMonitor; import com.fr.record.analyzer.EnableMetrics; import com.tptj.tool.hg.dynamic.agent.base.DynamicAgent; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021/9/11 **/ @EnableMetrics public class LifeCycle extends AbstractPluginLifecycleMonitor { public static final String PLUGIN_ID = "com.tptj.demo.hg.template.encrypt.v10"; public static final String PLUGIN_NAME = "template encrypt"; @Override @Focus(id = PLUGIN_ID, text = PLUGIN_NAME) public void afterRun(PluginContext context) { DynamicAgent.getInstance().register( new SpEncryptIOFileProxy(), new SpCptApp(), new SpFormApp()); } @Override public void beforeStop(PluginContext context) { DynamicAgent.getInstance().reset(); } } |
这样,在插件启动后,相关产品类方法执行时,就会触发我们的切面方法执行。(再次强调,此方法严禁用于商城插件中,极易产生不稳定风险)
然后利用IOFileAttrMark模板属性扩展接口,配合MenuHandler接口,为模板增加一个密码参数的配置属性
package com.tptj.demo.hg.template.encrypt.ui; import com.fr.json.JSONException; import com.fr.json.JSONObject; import com.fr.stable.StringUtils; import com.fr.stable.fun.impl.AbstractIOFileAttrMark; import com.fr.stable.xml.XMLPrintWriter; import com.fr.stable.xml.XMLableReader; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021/9/12 **/ public class SpEncryptAttrMark extends AbstractIOFileAttrMark { public final static String XML_TAG = "SpEncryptAttrMark"; private String secret; public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } @Override public String xmlTag() { return XML_TAG; } @Override public void readXML(XMLableReader reader) { String tag = reader.getTagName(); if( XML_TAG.equals(tag) ){ secret = reader.getAttrAsString("secret", StringUtils.EMPTY); } } @Override public void writeXML(XMLPrintWriter writer) { writer.startTAG(XML_TAG).attr("secret",secret).end(); } @Override public SpEncryptAttrMark clone() { SpEncryptAttrMark obj = (SpEncryptAttrMark)super.clone(); obj.secret = secret; return obj; } @Override public JSONObject createJSONConfig() throws JSONException { JSONObject json = super.createJSONConfig(); json.put("secret",secret); return json; } } |
package com.tptj.demo.hg.template.encrypt.ui; import com.fr.design.fun.impl.AbstractMenuHandler; import com.fr.design.mainframe.DesignerContext; import com.fr.design.mainframe.JTemplate; import com.fr.design.mainframe.toolbar.ToolBarMenuDockPlus; import com.fr.design.menu.ShortCut; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021/9/12 **/ public class MenuEncryptor extends AbstractMenuHandler { private static final int INSERT_POSITION = 2; @Override public int insertPosition(int i) { return INSERT_POSITION; } @Override public boolean insertSeparatorBefore() { return true; } @Override public boolean insertSeparatorAfter() { return false; } @Override public String category() { return TEMPLATE; } @Override public ShortCut shortcut(){ JTemplate template = DesignerContext.getDesignerFrame().getSelectedJTemplate(); return shortcut(template); } @Override public ShortCut shortcut(ToolBarMenuDockPlus plus) { //往ToolBarMenuDockPlus里塞感觉也很糟. if (!(plus instanceof JTemplate)){ return null; } return new MenuEncryptAction( (JTemplate)plus ); } } |
最后再使用TemplateEncryptProvider接口来接收收集到的密钥和其他信息,进行最终的加解密
package com.tptj.demo.hg.template.encrypt; import com.fr.general.CommonIOUtils; import com.fr.report.fun.impl.AbstractTemplateEncryptProvider; import com.fr.security.SecurityToolbox; import com.fr.stable.CodeUtils; import com.fr.stable.StringUtils; import java.io.ByteArrayInputStream; import java.io.InputStream; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021/9/12 **/ public class DemoEncryptor extends AbstractTemplateEncryptProvider { public static final String TAG_START = "-----Encrypt Start-----"; public static final String TAG_END = "-----Encrypt End-----"; public static ThreadLocal<String> holder = new ThreadLocal<String>(); public static ThreadLocal<Integer> pos = new ThreadLocal<Integer>(); public static ThreadLocal<Boolean> init = new ThreadLocal<Boolean>(); public static void setSecret(String secret) { holder.set(secret); } public static String getSecret() { return holder.get(); } public static boolean hasSecret() { return StringUtils.isNotEmpty(getSecret()); } public static int getPos(){ return pos.get(); } public static void setPos(int idx){ pos.set(idx); } public static void initData(InputStream in){ init.set(true); String data = StringUtils.EMPTY; try{ data = CommonIOUtils.inputStream2String(in); if(!data.startsWith(TAG_START)){ setSecret(StringUtils.EMPTY); setPos(-1); return; } int end = data.indexOf(TAG_END); String part = data.substring(TAG_START.length(),end); String sha = part.substring(0,64); String code = part.substring(64); String secret = CodeUtils.passwordDecode("___"+code); if( !CodeUtils.md5Encode(secret, StringUtils.EMPTY,"SHA-256").equals(sha) ){ setSecret(StringUtils.EMPTY); setPos(-1); return; } setSecret(secret); setPos(end+TAG_END.length()); }catch(Exception e){ setSecret(StringUtils.EMPTY); setPos(-1); } } private String codePassword(){ String secret = getSecret(); String code = CodeUtils.passwordEncode(secret).substring(3); String sha = CodeUtils.md5Encode(secret, StringUtils.EMPTY,"SHA-256"); return sha+code; } @Override public InputStream encode(InputStream in) { if( !hasSecret() ){ return in; } byte[] bytes = CommonIOUtils.inputStream2Bytes(in); try{ String secret = getSecret(); String data = new String(bytes,"UTF-8"); data = TAG_START +codePassword()+TAG_END+ SecurityToolbox.aesEncrypt( data, secret); return new ByteArrayInputStream(data.getBytes("UTF-8")); }catch (Exception e){ return new ByteArrayInputStream(bytes); } } @Override public InputStream decode(InputStream in) { byte[] bytes = CommonIOUtils.inputStream2Bytes(in); try{ if( !Boolean.TRUE.equals(init.get()) ){ initData(new ByteArrayInputStream(bytes)); } if( !hasSecret() ){ return new ByteArrayInputStream(bytes); } String data = new String(bytes,"UTF-8"); data = data.substring( getPos() ); data = SecurityToolbox.aesDecrypt( data, getSecret() ); return new ByteArrayInputStream(data.getBytes("UTF-8")); }catch (Exception e){ return new ByteArrayInputStream(bytes); } } } |
这样开发者就可以为每个自己编辑的模板进行加密了,再也不用担心别人拿去随便就能篡改了。这类方案还可扩展实现包括模板授权在内的相关场景。不过设计上就更为复杂了。
注意点:
1.考虑到整个模板在一个线程内,是线性的读取和保存的。而TemplateEncryptProvider接口本身的参数只有输入流本身,那么包括当前编辑者,模板路径、类型、ID等等信息,本身无法直接传入,所以使用了ThreadLocal来实现线程内的参数传递。
2.在本例中并未涉及模板授权的的内容,那么需要考虑,用户实际部署模板后预览时,本身是不存在输入密码这个动作的(当然开发者也可以基于此,涉及一套通过输入模板密钥来预览模板的权限方式)。所以实现时需要考虑解密本身不依赖于输入的密码,所以本例直接把密码加密后跟模板保存在一起了,这种方案的安全性是依赖于对密码加密的算法本身的安全性和其他二级密钥来保障的。故如果是商用,那么实现的算法源码一定是不能开源的,否则就不安全了
3.在注入切面时,尽量以少影响少注入为原则去选择,注入点。注入点越多,方案越不稳定。
免责声明:所有文档中的开源示例,均为开发者自行开发并提供。仅用于参考和学习使用,开发者和官方均无义务对开源案例所涉及的所有成果进行教学和指导。若作为商用一切后果责任由使用者自行承担。
demo-parameter-decode【两种接口实现的基于MD5的密码防篡改代码】