上周在项目中需要对URL参数进行加密传输,实际过程中碰到了一些问题,在此对加密算法的Java实现及出现的编码问题进行一个简单的记录。
一、加密算法
这次分别对RSA(非对称加密)和AES(对称加密)进行了使用。这里也只对这两种算法的Java实现进行简单介绍,网上资料满天飞,算法的具体内容和其他的算法自行查找吧。
RSA,通常使用公钥加密、私钥解密,反之亦然;而且大家肯定是不希望有人冒充我们发消息,可以通过只有我们自己掌握的私钥来负责签名,公钥负责验证。通常私钥长度有1024bit,2048bit,4096bit,长度越长,越安全,但是生成密钥越慢,加解密也越耗时(当然生成的加密串的长度也同所选秘钥的长度一致)。
//生成秘钥
public StringgenerateKey() {
try {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); //采用RSA算法
kpg.initialize(1024); //初始化KeyPairGenerator对象,密钥长度采用1024bit
KeyPair kp = kpg.genKeyPair(); //生成秘钥对
RSAPublicKey pbkey = (RSAPublicKey) kp.getPublic(); //获取公钥
RSAPrivateKey prkey = (RSAPrivateKey) kp.getPrivate(); //获取私钥
// 通过base64编码得到公钥字符串
String publicKeyString = org.apache.tomcat.util.codec.binary.Base64.encodeBase64String(pbkey.getEncoded());
// 通过base64编码得到私钥字符串
String privateKeyString = org.apache.tomcat.util.codec.binary.Base64.encodeBase64String(prkey.getEncoded());
return "publicKeyString:"+publicKeyString+" privateKeyString:"+privateKeyString;
}catch (Exception e) {
return null;
}
}
//我这里是将之前生成的公钥、私钥保存在配置文件中了,现在通过@Value()注解来获取秘钥
@Value("${active.pbkey}")private String pbkey;
@Value("${active.prkey}")private String prkey;
//使用公钥加密
public byte[](@RequestParam String accountName)throws Exception {
//将base64编码后的公钥字符串转成PublicKey实例(公钥要通过X509编码的key来获取)
byte[] buffer = org.apache.tomcat.util.codec.binary.Base64.decodeBase64(pbkey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpec =new X509EncodedKeySpec(buffer);
RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
//加密
Cipher cipher =null;
try {
cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[]result = cipher.doFinal(accountName.getBytes());
return result;
}catch (Exception e) {
log.error("参数加密失败", e);
return null;
}
}
//使用私钥进行解密
public String(@RequestParam byte[]url)throws Exception {
//将base64编码后的私钥字符串转成PrivateKey实例(私钥要通过PKCS#8 编码的key来获取)
byte[] buffer = Base64.decodeBase64(prkey);
PKCS8EncodedKeySpec keySpec =new PKCS8EncodedKeySpec(buffer);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
//解密
Cipher cipher =null;
try {
cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
String accountName=new String(cipher.doFinal(url));
return accountName;
}catch (NoSuchPaddingException e) {
log.error("参数解密失败", e);
return null;
}
}
AES,密钥最长只有256个bit,执行速度快。由于是对称加密,是没有公钥和私钥的区分的,双方使用同一秘钥进行加密、解密,安全度相对非对称加密较低。基于以上特点,通常使用RSA来首先传输AES的密钥给对方(速度慢,安全性高),然后再使用AES来进行加密通讯(速度快,安全性较低)。
//生成AES秘钥,AES没有秘钥对,直接生成秘钥即可
public StringgenerateKey() {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(128);
SecretKey secretKey = keyGenerator.generateKey(); //生成秘钥
StringKeyString= Base64.encodeBase64String(secretKey.getEncoded()); // 得到密钥字符串
return "KeyString:"+KeyString;
}catch (Exception e) {
return null;
}
}
//获取存储在配置文件中的秘钥
@Value("${active.key}")
private String key;
// 加密.
public byte[]encrypt(String refer) {
byte[] buffer = Base64.decodeBase64(key);
SecretKey key=new SecretKeySpec(buffer, "AES");
Cipher cipher =null;
try {
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[]result = cipher.doFinal(refer.getBytes("UTF-8"));
return result;
}catch (Exception e) {
log.error("参数加密失败", e);
return null;
}
}
//解密.
public String decrypt(byte[]refer) {
byte[] buffer = Base64.decodeBase64(key);
SecretKey key=new SecretKeySpec(buffer, "AES");
Cipher cipher =null;
try {
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
String url = new String(cipher.doFinal(refer),"UTF-8");
return url;
}catch (Exception e) {
log.error("参数解密失败,错误refer:"+refer, e);
return null;
}
}
二、常见问题
因为生成的密文为byte[ ]类型,如果使用上面的代码,直接对加密后的byte[ ]密文进行解密是完全没有问题的。但我们实际使用中经常需要以Strring类型进行传输,需要通过url传输后再解密,这种情况下会出现很多问题。
我们可以明显的看出,经过String转换得到的result已与初始的bytes不同了。原因是转换为String时是根据当前默认编码类型(UTF-8)来生成的,UTF-8是可变长度的编码,有的字符需要用多个字节来表示,所以也就出现了在转换之后byte[]数组长度、内容不一致的情况。
解决方案:
(1)Base64
Base64 是一种将二进制数据编码的方式,正如UTF-8和UTF-16是将文本数据编码的方式一样,我们可以通过Base64将二进制数据编码为文本数据。
//加密后将byte[ ]密文通过Base64转为String
String str = Base64.encodeBase64String(bytes);
//解密前将String再通过Base64解码为byte[ ]
byte[ ] bytes = Base64.decodeBase64(str);
***需要注意的是,Base64编码后可能出现字符+和/,在URL中就不能直接作为参数,因为在urlEcode编码中 “+” 会被解码成空格。
解决方案一:拿到数据时将空格替换回“+”
解决方案二:预先进行urlEncode(但是如果该编码后的密文在服务端获取到之前经过微信、QQ转发或在浏览器中重定向后会被提前decode,服务端拿到后仍不能正常解析)
//加密、Base64编码后先encode再通过URL传输
String str = URLEncoder.encode(Base64.encodeBase64String(bytes),"UTF-8");
//解密前直接Base64解码即可,经过URL传输后获得的链接已decode
byte[ ] bytes = Base64.decodeBase64(str);
解决方案三:使用URL安全的Base64编码,会把字符+和/分别变成-和_
//加密后使用URLSafeBase64
String str = Base64.encodeBase64URLSafeString(bytes);
//解密前先解码
byte[ ] bytes = Base64.decodeBase64(str);
(2)转换进制
为了防止二进制直接转为字符串String类型时出现数据缺失的现象,先byte[ ]密文转换为十六进制,解密前再将十六进制转回二进制。
Java中的String对象是不需要指定编码表的,因为String里的字符信息是用UNICODE编码的,并且Java使用char数据类型来对应UNICODE的字符,其大小为固定的两个8位16进制数字。Java中byte用二进制表示占用8位,而我们知道16进制的每个字符需要用4位二进制位来表示。所以我们就可以把每个byte转换成两个相应的16进制字符,即把byte的高4位和低4位分别转换成相应的16进制字符H和L,并组合起来得到byte转换到16进制字符串的结果new String(H) + new String(L)。同理,相反的转换也是将两个16进制字符转换成一个byte,原理同上。根据以上原理,我们就可以将byte[] 数组转换为16进制字符串了,当然也可以将16进制字符串转换为byte[]数组了。
//加密后使用转为十六进制
String str = XXClass.parseByte2HexStr( bytes );
//解密前先转回二进制
byte[ ] bytes = XXClass .parseHexStr2Byte(str);
//2转16
public static StringparseByte2HexStr(byte buf[]) {
StringBuffer sb =new StringBuffer();
for (int i =0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] &0xFF);
if (hex.length() ==1) {
hex ='0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
//16转2
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[i] = (byte) (high *16 + low);
}
return result;
}
***附String的转换使用:
public static void main(String[] args){
String str ="ccha1994";
byte[] strbyte = str.getBytes();
System.out.println("toString:"+strbyte.toString());
System.out.println("new String:"+new String(strbyte));
}
运行结果:
toString():显示的结果用的是父类Object的toString()方法,通常默认返回当前对象(c)的内存地址,即hashCode。
new String():通过字节数组byte[]调用String对象中的toString(),是根据parameter是一个字节数组,使用java虚拟机默认的编码格式或者参数指定的编码格式,将这个字节数组decode为对应的字符。
使用:
new String()一般使用字符转码的时候,byte[ ]数组的时候。
toString()将对象打印的时候使用 。