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

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

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()将对象打印的时候使用 。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 概述 之前一直对加密相关的算法知之甚少,只知道类似DES、RSA等加密算法能对数据传输进行加密,且各种加密算法各有...
    Henryzhu阅读 3,001评论 0 14
  • Base64.java public final class Base64 { static private ...
    BUG弄潮儿阅读 779评论 0 0
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,587评论 18 139
  • 当你迷茫时, 阅读吧。 当你痛苦时, 阅读吧。 当你孤独时, 阅读吧。 刷新所有的朋友圈, 无法释怀的情绪, 在阅...
    麻小宝007阅读 209评论 0 0
  • 不知道为什么,这几天自己总是情绪低落,总是不由自主的想到老爸,想到他在时对我的种种呵护。或许我是累了,又或许我伤心...
    玺玺公主阅读 358评论 0 1