数据加密与字符编码的踩坑记录

    上周在项目中需要对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传输后再解密,这种情况下会出现很多问题。

数据加密与字符编码的踩坑记录_第1张图片
byte[ ]、String转换测试

     我们可以明显的看出,经过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()将对象打印的时候使用 。

你可能感兴趣的:(数据加密与字符编码的踩坑记录)