润乾报表跨系统调用的时候如何保证 URL 安全性
润乾报表与用户的系统集成,一般有两种方案。一是集成到用户系统中。二是将润乾报表单独部署到一个应用下。
两种方案各有利弊:第一种方案对于安全性等方面可以统一管理,但是报表本身如果数据量大并发大造成的压力会直接影响用户自己的系统。第二种方案在报表服务器承受压力过大数据量过大的时候,不会影响到用户本身的系统,也就是说就算报表服务器压力饱和进入等待状态,用户的系统也可以正常的使用,众所周知润乾报表的调用一般直接通过 url 的方式进行调用,那么第二种方案中的 url 就无法被用户系统所控制,造成了一定的安全隐患。
那么如何解决 URL 调用安全问题?
例如:
通过用户系统访问一张报表时,当然会带着一些用户信息或是其他参数传递过来。如果这时这个 url 的明文被其他人记住,比如 url 拼参数 username=zhangsan,这时李四记住了这个 url 并且与自己访问该资源的 url 进行对比发现传入参数为用户姓名拼音,由于采用的是第二种方案,用户系统并不能控制,这样就可以肆无忌惮的去查看其他人的信息,导致信息的泄漏。
解决思路:
这时我们可以采取对 url 中的明文进行加解密的操作,通过用户系统传递参数之前进行加密,然后传递到报表服务器端,在 jsp 中或者报表中进行解密。(下文介绍是在报表中解密)
完整示例:
shili.zip
实现方法:
一、首先确定加解密规则,这里使用的是 AES 加解密方法,密钥为 16 位英文,由于是说明将密钥定义死在相应类中。
二、通过测试类将 username=zhangsan 加密为:
880CB3013AB0D0F3A91B1C36B323F6E40294D3BE6EEFAA38135690CA3DCC61A2 模拟用户系统传递参数到报表服务器的 url 为:
三、编写 AES 加解密的自定义函数,接受参数时只用到解密函数,加密函数是在报表中超链接等位置再次使用时可以调用加密函数。
1、实现 AES 加解密的类
public class AES {
/**
\* 加密
*
\* @param content 需要加密的内容
\* @param password 加密密码
\* @return
*/
public static byte\[\] encrypt(String content, String password) {
try {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(password.getBytes()));
SecretKey secretKey = kgen.generateKey();
byte\[\] enCodeFormat = secretKey.getEncoded();
SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");// 创建密码器
byte\[\] byteContent = content.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, key);// 初始化
byte\[\] result = cipher.doFinal(byteContent);
return result; // 加密
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
/**解密
\* @param content 待解密内容
\* @param password 解密密钥
\* @return
*/
public static byte\[\] decrypt(byte\[\] content, String password) {
try {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(password.getBytes()));
SecretKey secretKey = kgen.generateKey();
byte\[\] enCodeFormat = secretKey.getEncoded();
SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");// 创建密码器
cipher.init(Cipher.DECRYPT_MODE, key);// 初始化
byte\[\] result = cipher.doFinal(content);
return result; // 加密
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
/**将二进制转换成16进制
\* @param buf
\* @return
*/
public static String parseByte2HexStr(byte buf\[\]) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf_& 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
/**将16进制转换为二进制
\* @param hexStr
\* @return
*/
public static byte\[\] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1)
return null;
byte\[\] result = new byte\[hexStr.length()/2\];
for (int i = 0;i< hexStr.length()/2; i++) {
int high = Integer.parseInt(hexStr.substring(i\*2, i\*2+1), 16);
int low = Integer.parseInt(hexStr.substring(i\*2+1, i\*2+2), 16);
result_= (byte) (high * 16 + low);
}
return result;
}
2、润乾自定义加解密函数中调用 AES 类
加密自定义函数:
//判断参数个数
if (this.param == null || this.param.getSubSize() !=0) {
MessageManager mm = EngineMessage.get();
throw new ReportError("encrypt:" + mm.getMessage("function.missingParam"));
}
//取得第一个参数,默认为表达式,需要把该表达式算出来,结果才是函数的参数值
Expression param1=(Expression)this. param.getLeafExpression();
if (param1 == null) { //判断参数是否为空
MessageManager mm = EngineMessage.get();
throw new ReportError("encrypt:" + mm.getMessage("function.invalidParam"));
}
//算出第一个参数值
Object result1 = Variant2.getValue(param1.calculate(ctx), false);
//判断第一个参数值是否为空
if (result1 == null) {
return null;
}
//判断第一个参数值的数据类型
if (! (result1 instanceof String)) {
MessageManager mm = EngineMessage.get();
throw new ReportError("encrypt:" + mm.getMessage("function.paramTypeError"));
}
// 算出第一个参数值
Object result2 = Variant2.getValue(param1.calculate(ctx),false);
// 判断第一个参数值是否为空
if (result2 == null) {
return null;
}
AES aes = new AES();
byte\[\] value = aes.encrypt(result2.toString(),"aaaaaaaaaaaaaaaa");
String encryptResultStr = aes.parseByte2HexStr(value);
return encryptResultStr;
解密自定义函数:
//判断参数个数
if (this.param == null || this.param.getSubSize() !=0) {
MessageManager mm = EngineMessage.get();
throw new ReportError("encrypt:" + mm.getMessage("function.missingParam"));
}
//取得第一个参数,默认为表达式,需要把该表达式算出来,结果才是函数的参数值
Expression param1=(Expression)this. param.getLeafExpression();
if (param1 == null) { //判断参数是否为空
MessageManager mm = EngineMessage.get();
throw new ReportError("encrypt:" + mm.getMessage("function.invalidParam"));
}
//算出第一个参数值
Object result1 = Variant2.getValue(param1.calculate(ctx), false);
//判断第一个参数值是否为空
if (result1 == null) {
return null;
}
//判断第一个参数值的数据类型
if (! (result1 instanceof String)) {
MessageManager mm = EngineMessage.get();
throw new ReportError("encrypt:" + mm.getMessage("function.paramTypeError"));
}
// 算出第一个参数值
Object result2 = Variant2.getValue(param1.calculate(ctx),false);
// 判断第一个参数值是否为空
if (result2 == null) {
return null;
}
AES aes = new AES();
byte\[\] decryptFrom = aes.parseHexStr2Byte(result2.toString());
byte\[\] decryptResult = aes.decrypt(decryptFrom,"aaaaaaaaaaaaaaaa");
return new String(decryptResult);
四、 自定义函数注册
1,将编译后的 class 文件复制到
\report5\web\webapps\demo\WEB-INF\classes
2,在
\report5\web\webapps\demo\WEB-INF\classes\config\customFunctions.properties 文件加入以下内容.
encode=0,api.MyEncode
decode=0,api.MyDecode
3,如果 \WEB-INF\classes 目前下没有 config 目录,则可以自己创建.
如果已经存在,在该目录下执行上述步骤 2.
重新启动设计器服务器。
在设计器中测试内容:
=encode(“ceshi”);
=decode(“555DF8AB73DAB1FF191A2B977430E02B”);
出现结果为正确.
五、报表中设定参数 aes 接受 url 中传递过来的值,通过动态参数调用解密函数将真实值取到进行报表中的运算。
中文解码会乱码,我这里编译不了 java 文件,可以帮忙改一下中文编码吗