diff --git a/README.md b/README.md index fb66ad6..4914b0c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # demo-parameter-decode -参数解密demo示例 \ No newline at end of file +参数解密demo示例\ +参数加密传输场景中的,通过签名算法保障参数表不被篡改和有效期控制的方案示例代码\ +plugin.xml中两个接口实际使用时只需要选择其中一个即可。 \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..5231b5d --- /dev/null +++ b/build.gradle @@ -0,0 +1,124 @@ + +apply plugin: 'java' + +[compileJava,compileTestJava]*.options*.encoding = 'UTF-8' + +ext { + /** + * 项目中依赖的jar的路径 + * 1.如果依赖的jar需要打包到zip中,放置在lib根目录下 + * 2.如果依赖的jar仅仅是编译时需要,防止在lib下子目录下即可 + */ + libPath = "$projectDir/../../webroot/WEB-INF/lib" + + /** + * 是否对插件的class进行加密保护,防止反编译 + */ + guard = false + + def pluginInfo = getPluginInfo() + pluginPre = "fine-plugin" + pluginName = pluginInfo.id + pluginVersion = pluginInfo.version + + outputPath = "$projectDir/../../webroot/WEB-INF/plugins/plugin-" + pluginName + "-1.0/classes" +} + +group = 'com.fr.plugin' +version = '10.0' +sourceCompatibility = '8' + +sourceSets { + main { + java.outputDir = file(outputPath) + output.resourcesDir = file(outputPath) + } +} + +ant.importBuild("encrypt.xml") +//定义ant变量 +ant.projectDir = projectDir +ant.references["compile.classpath"] = ant.path { + fileset(dir: libPath, includes: '**/*.jar') + fileset(dir: ".",includes:"**/*.jar" ) +} + +classes.dependsOn('clean') + +task copyFiles(type: Copy,dependsOn: 'classes'){ + from outputPath + into "$projectDir/classes" +} + +task preJar(type:Copy,dependsOn: guard ? 'compile_encrypt_javas' : 'compile_plain_javas'){ + from "$projectDir/classes" + into "$projectDir/transform-classes" + include "**/*.*" +} +jar.dependsOn("preJar") + +task makeJar(type: Jar,dependsOn: preJar){ + from fileTree(dir: "$projectDir/transform-classes") + baseName pluginPre + appendix pluginName + version pluginVersion + destinationDir = file("$buildDir/libs") + + doLast(){ + delete file("$projectDir/classes") + delete file("$projectDir/transform-classes") + } +} + +task copyFile(type: Copy,dependsOn: ["makeJar"]){ + from "$buildDir/libs" + from("$projectDir/lib") { + include "*.jar" + } + from "$projectDir/plugin.xml" + into file("$buildDir/temp/plugin") +} + +task zip(type:Zip,dependsOn:["copyFile"]){ + from "$buildDir/temp/plugin" + destinationDir file("$buildDir/install") + baseName pluginPre + appendix pluginName + version pluginVersion +} + +//控制build时包含哪些文件,排除哪些文件 +processResources { +// exclude everything +// 用*.css没效果 +// exclude '**/*.css' +// except this file +// include 'xx.xml' +} + +/*读取plugin.xml中的version*/ +def getPluginInfo(){ + def xmlFile = file("plugin.xml") + if (!xmlFile.exists()) { + return ["id":"none", "version":"1.0.0"] + } + def plugin = new XmlParser().parse(xmlFile) + def version = plugin.version[0].text() + def id = plugin.id[0].text() + return ["id":id,"version":version] +} + +repositories { + mavenLocal() + maven { + url = uri('http://mvn.finedevelop.com/repository/maven-public/') + } +} + +dependencies { + //使用本地jar + implementation fileTree(dir: 'lib', include: ['**/*.jar']) + implementation fileTree(dir: libPath, include: ['**/*.jar']) +} + + diff --git a/encrypt.xml b/encrypt.xml new file mode 100644 index 0000000..1401cd1 --- /dev/null +++ b/encrypt.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugin.xml b/plugin.xml new file mode 100644 index 0000000..a432fe8 --- /dev/null +++ b/plugin.xml @@ -0,0 +1,20 @@ + + com.tptj.demo.hg.parameter.decode.v10 + + yes + 1.0 + 10.0 + tptj + 2019-07-18 + + + com.tptj.demo.hg.parameter.decode + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/tptj/demo/hg/parameter/decode/DecodeRequest.java b/src/main/java/com/tptj/demo/hg/parameter/decode/DecodeRequest.java new file mode 100644 index 0000000..4fe0938 --- /dev/null +++ b/src/main/java/com/tptj/demo/hg/parameter/decode/DecodeRequest.java @@ -0,0 +1,103 @@ +package com.tptj.demo.hg.parameter.decode; + +import com.fr.data.DefaultRequestParameterHandler; +import com.fr.stable.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.util.*; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021/7/11 + **/ +public class DecodeRequest extends HttpServletRequestWrapper { + + private HttpServletRequest request; + + public DecodeRequest(HttpServletRequest request) { + super(request); + this.request = request; + } + + /** + * 存放验签的结果 + */ + private boolean result = true; + /** + * 受保护的参数表 + */ + private Set parameter_names = new TreeSet(); + + private Map parameters= new HashMap(); + + private Object getSuperValue(String name){ + Object val = super.getHeader(name); + if( null != val ){ + return val; + } + val = super.getParameter(name); + if( null != val ){ + return val; + } + return new DefaultRequestParameterHandler().getParameterFromJSONParameters(request,name); + } + + private void init(){ + parameter_names = Demo1.getTagNames( (HttpServletRequest)getRequest() ); + String sign_str = (String) getSuperValue( "sign"); + Sign sign = Sign.parse(sign_str); + //过有效期 + if( sign.isTimeout() ){ + result = false; + }else{ + StringBuilder sb = new StringBuilder(); + for( String pname : parameter_names ){ + Object val = getSuperValue(pname); + if( null != val){ + sb.append(pname).append(val); + } + } + result = sign.check( sb.toString(), Demo1.SECRET ); + } + //调整参数表 + Set> entries = getParameterMap().entrySet(); + for( Map.Entry entry : entries ){ + String key = entry.getKey(); + String[] value = entry.getValue(); + //验签失败的情况下,所有的涉敏参数都要设置成空的 + if( !result && parameter_names.contains(key) ){ + parameters.put( key, new String[]{StringUtils.EMPTY}); + }else{ + parameters.put( key, value); + } + } + } + + @Override + public String getParameter(String name){ + if( parameters.containsKey(name) ){ + return parameters.get(name)[0]; + } + return null; + } + + @Override + public String getHeader(String name){ + if( !result && parameter_names.contains(name) ){ + return StringUtils.EMPTY; + } + return super.getHeader(name); + } + + @Override + public Map getParameterMap() { + return parameters; + } + + @Override + public String[] getParameterValues(String name) { + return parameters.get(name); + } +} diff --git a/src/main/java/com/tptj/demo/hg/parameter/decode/Demo1.java b/src/main/java/com/tptj/demo/hg/parameter/decode/Demo1.java new file mode 100644 index 0000000..625a680 --- /dev/null +++ b/src/main/java/com/tptj/demo/hg/parameter/decode/Demo1.java @@ -0,0 +1,121 @@ +package com.tptj.demo.hg.parameter.decode; + +import com.fr.base.BaseUtils; +import com.fr.data.DefaultRequestParameterHandler; +import com.fr.general.data.DataModel; +import com.fr.script.Calculator; +import com.fr.stable.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Set; +import java.util.TreeSet; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021/7/11 + * MD5签名的参数获取方案校验,注意该代码仅仅是教学演示用! + * 并非完整可靠的方案,要实际运用在项目中,要自行根据项目需要,进行相应改造! + **/ +public class Demo1 extends DefaultRequestParameterHandler { + public static final String DS_NAME = "ProtectedParameterNames"; + /** + * 假设密钥固定是123456,实际开发时可用配置的方式实现 + */ + public static final String SECRET = "123456"; + /** + * 假设有一个服务器数据集名字叫ProtectedParameterNames,数据集第一列就是一组参数名,里面所有的参数都禁止用户自己选择和传递! + * @param req + * @return + */ + public static Set getTagNames(HttpServletRequest req){ + Set set = (Set)req.getSession(true).getAttribute(DS_NAME); + if( null != set ){ + return set; + } + //需要排序 + set = new TreeSet(); + try{ + DataModel dm = BaseUtils.getDataModelFromTableDataName(Calculator.createCalculator(), DS_NAME); + for(int i=0,len=dm.getRowCount(); i pnames = getTagNames(req); + if( !pnames.contains(name) ){ + return value; + } + String sign_str = (String) getSuperValue(req, "sign"); + Sign sign = Sign.parse(sign_str); + //过有效期 + if( sign.isTimeout() ){ + return StringUtils.EMPTY; + } + StringBuilder sb = new StringBuilder(); + for( String pname : pnames ){ + Object val = getSuperValue(req,pname); + if( null != val){ + sb.append(pname).append(val); + } + } + //签名不合法 + if( !sign.check( sb.toString(), SECRET ) ){ + return StringUtils.EMPTY; + } + return value; + } + + @Override + public Object getParameterFromRequest(HttpServletRequest req, String name) { + Object value = super.getParameterFromRequest(req,name); + return getResult(req,name,value); + } + + @Override + public Object getParameterFromHeader(HttpServletRequest req, String name) { + Object value = super.getParameterFromHeader(req,name); + return getResult(req,name,value); + } + + @Override + public Object getParameterFromJSONParameters(HttpServletRequest req, String name){ + //FR在设计的时候,参数不仅仅可以直接在query中通过参数名直接给到,还可以通过 __parameters__ 给到一个json字符串来提个一个额外的参数表 + //同一个参数名的参数优先级 header > query > attribute > json > session + Object value = super.getParameterFromJSONParameters(req,name); + return getResult(req,name,value); + } + + @Override + public Object getParameterFromRequestInputStream(HttpServletRequest req, String name){ + //这里单独实现这个方法是强调一下,在10.0版本中,该方法已经没有用了。开发者不要考虑通过这个方法从body中抽取参数 + //如果要使用这个方法,就需要使用filter先把请求的input stream 改成可重复读取的。否则平台请求会报错! + //但是如果都用filter去处理参数了,我们就完全没必要使用这个接口了。 + return super.getParameterFromRequestInputStream(req,name); + } +} diff --git a/src/main/java/com/tptj/demo/hg/parameter/decode/Demo2.java b/src/main/java/com/tptj/demo/hg/parameter/decode/Demo2.java new file mode 100644 index 0000000..9760f26 --- /dev/null +++ b/src/main/java/com/tptj/demo/hg/parameter/decode/Demo2.java @@ -0,0 +1,35 @@ +package com.tptj.demo.hg.parameter.decode; + +import com.fr.decision.fun.impl.AbstractGlobalRequestFilterProvider; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021/7/11 + **/ +public class Demo2 extends AbstractGlobalRequestFilterProvider { + @Override + public String filterName() { + return "Parameter Decode"; + } + + @Override + public String[] urlPatterns() { + return new String[]{ + "/*" + }; + } + + public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) { + req = new DecodeRequest(req); + try{ + chain.doFilter(req, res); + }catch (Exception e){ + + } + } +} diff --git a/src/main/java/com/tptj/demo/hg/parameter/decode/Sign.java b/src/main/java/com/tptj/demo/hg/parameter/decode/Sign.java new file mode 100644 index 0000000..0fab92e --- /dev/null +++ b/src/main/java/com/tptj/demo/hg/parameter/decode/Sign.java @@ -0,0 +1,65 @@ +package com.tptj.demo.hg.parameter.decode; + +import com.fr.intelli.record.Focus; +import com.fr.record.analyzer.EnableMetrics; +import com.fr.stable.CodeUtils; +import com.fr.stable.StringUtils; + +/** + * @author 秃破天际 + * @version 10.0 + * Created by 秃破天际 on 2021/7/11 + **/ +@EnableMetrics +public class Sign { + private String md5; + private long timestamp; + private long timeout; + + private Sign(String md5, long timestamp, long timeout) { + this.md5 = md5; + this.timestamp = timestamp; + this.timeout = timeout; + } + + private final static Sign EMPTY = new Sign(StringUtils.EMPTY,0,0); + + /** + * 解析签名 + * @param sign + * @return + */ + public static Sign parse( String sign ){ + try{ + String md5 = sign.substring(0,32); + long timestamp = Long.parseLong( sign.substring(33,46) ); + long timeout = Long.parseLong( sign.substring(47) ); + return new Sign(md5,timestamp,timeout); + }catch(Exception e){ + + } + return EMPTY; + } + + /** + * 检验超时 + * @return + */ + public boolean isTimeout(){ + long crt = System.currentTimeMillis(); + return crt < timestamp || crt - timestamp > timeout; + } + + /** + * 验签 + * @param data + * @param secret + * @return + */ + @Focus(id = "com.tptj.demo.hg.parameter.decode.v10",text = "Parameter Decode") + public boolean check( String data, String secret ){ + String source = data+secret+timestamp+timeout; + String md5 = CodeUtils.md5Encode(source, StringUtils.EMPTY,"MD5"); + return StringUtils.equals(md5,this.md5); + } +}