• JavaWeb对RSA的使用


    由于公司的网站页面的表单提交是明文的post,虽说是https的页面,但还是有点隐患(https会不会被黑?反正明文逼格是差了点你得承认啊),所以上头吩咐我弄个RSA加密,客户端JS加密,然后服务器JAVA解密

    本文主要面向想在javaweb/java应用里面使用RSA的人。

    一、RSA是个ShenMeGui:

    其实一开始叫我用RSA加密我是拒绝的,因为不可能你叫我用我就用,我得查查他是什么东西对不对。

    RSA是目前最有影响力的公钥加密算法,属于非对称加密的,也就是用一个大家都知道的公钥来加密出来的密文,只有拥有私钥的人才能解开,目前听说1024比较安全,2048位那是相当安全,往上就更难破解了。

    关于RSA的基本原理,百度百科里面提到“RSA算法基于一个十分简单的数论事实:将两个大素数相乘十分容易,但是想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。”然后阮一峰先生的 《RSA算法原理(一)》《RSA算法原理(二)》里面就解释的比较清楚,看完你大概就懂了(不愧是大牛吖)。阮先生提到了:

    加密公式

    me ≡ c (mod n)

    解密公式

    cd ≡ m (mod n)

    n是公共的模数,也就是pq的乘积,e是公钥的指数,d是私钥的指数,下面会说到利用他们可以还原公钥和密钥。

    如果你知道是怎么回事的话也就清楚了RSA是怎么回事, 不清楚的话请去看文章,这里主要是说如何去使用RSA而不是自己实现RSA。知道RSA的大概原理将会对我们写代码有帮助,不然你出了问题自己都搞不定,咋办?

    二、使用RSA实现加密解密的思路:

    说起思路比较简单。其实就是浏览器向服务器拿到公钥,在用户填完信息后,用公钥帮用户加密,然后提交后,服务器再用java解密就可以了…….吗?本来大体思路是这样没错,可是我找到的方法并没有办法直接传输可用的公钥,所以我们要用到上述的公式,先把e和n取出来,利用js把e和n还原成公钥,然后再用他给信息加密后提交,最后就是用服务器上的java解密。

    三、具体实施方法:

    说了那么多,福利呢干货呢别人写好的代码呢?没错我就知道你想要这个。

    一开始我也是大概搞清楚RSA之后开始各种找代码来参考一下,于是在我不懈努力之下,让我找到了这个《用javascript与java进行RSA加密与解密》,总体还是可用的,而且总体思路和我一致,但是用起来问题还是多多的,你们可以先点开链接看看代码。

    四、javascript里面的RSA:

    首先你需要三个js

    BigInt.js  – 用于生成一个大整型;(这是RSA算法的需要)
    RSA.js    – RSA的主要算法;
    Barrett.js – RSA算法所需要用到的一个支持文件;

    像上面提到的,你拥有公钥的n和e就能还原公钥,然后进行加密。关键代码如下:

    //setMaxDigits()貌似是生成密文的最大位数,如果不设置或者乱设置的话很可能导致死循环然后浏览器崩溃。
    //这个语句必须最先执行,1024位的密钥要传入130,2048的话要传入260
    setMaxDigits(130);
    //RSAKeyPair是密钥对的对象,用e和n可以生成公钥,第二个参数其实就是d,因为我们只需要公钥当然是传空的。
    key = new  RSAKeyPair(e,"",n);
    //下面是加密方法,比较简单,就是传入公钥和原文,加密成密文。
    var result = encryptedString(key, document.getElementById("pwd").value);

    e和n哪里来?你可以先用着链接里面的来test一下,我认为正常来说应该是java读取了送到给js的,下面会讲到。

    五、 java里面的RSA:

    那java里面肯定也有RSA的相关类和API能用,我们需要些什么呢?我们需要的是JDK里面的java.security包和一个开源的加解密解决方案BouncyCastle的API。java.security自己import就可以,→bouncyCastle在这里←    要JDK15以上的,下最新的就可以,打不开或者找不到的就百度一下咯~

    下面我们说说java里面的代码问题。

    先来说生成密钥,利用generateKeyPair()和saveKey(),可以很简单的生成一对密钥,你可以直接存到相应路径,也可以像我一样改一下saveKey()把公钥私钥分开储存,甚至你可以直接保存在session,这样你的网站就永远在用别人猜不到的密钥了(我估计这样服务器工作量略大,还不如定时线程来更新密钥),下面是我修改过的代码

    public static KeyPair generateKeyPair() throws Exception { 
        try { 
           KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider()); 
           final int KEY_SIZE = 1024;// 块加密的大小,你可以改成2048,但是会很慢........
           keyPairGen.initialize(KEY_SIZE, new SecureRandom()); 
           KeyPair keyPair = keyPairGen.generateKeyPair(); 
           return keyPair; 
        } catch (Exception e) { 
        throw new Exception(e.getMessage()); 
        } 
    }

    再说取钥的问题,你可以使用getKeyPair()获取公钥和密钥,大概长这样(我同事帮忙修改了下,返回的object然后自自行强制转换即可)这里注意readObject其实和加密包会有关系。

    public static Object getKey(String path)throws Exception{ 
         FileInputStream fis = new FileInputStream(path); 
         ObjectInputStream oos = new ObjectInputStream(fis); 
         Object kp=  oos.readObject(); 
         oos.close(); 
         fis.close(); 
         return kp; 
    }  
    //使用他来获取密钥,如果你这个路径对应的是公钥或者密钥,也可以直接取出来再强制转换成RSAPublicKey或者RSAPrivateKey
    KeyPair kp=(KeyPair)getKey("D:/key.key");

    我们读取到公钥后,需要用到RSAPublicKey的getModulus()和getPublicExponent()方法取得公钥的e(Exponent)和n(Modulus)给到前端页面,前端可以用getparameter等方法接收,或者在页面初始化时用ajax请求。

    String Modulus=RSAPublicKey.getModulus().toString(16);
    String Exponent=RSAPublicKey.getPublicExponent().toString(16);

    这里需要toString(16)把他转为16进制,供前端使用。

    最后是最重要的解密部分(加密和解密的写法极其相似,有需要的同学可以先看看解密),网上最简单而典型的写法是这样的(raw是密文)

    public static byte[] RSAdecrypt(PrivateKey pk, byte[] raw) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA", new BouncyCastleProvider());
        cipher.init(Cipher.DECRYPT_MODE, pk); 
        return cipher.doFinal(raw);
    }

    然后我在网上找到demo是这样的

    public static byte[] RSAdecrypt(PrivateKey pk, byte[] raw) throws Exception {
      Cipher cipher = Cipher.getInstance("RSA", new BouncyCastleProvider());
      cipher.init(Cipher.DECRYPT_MODE, pk);      
      ByteArrayOutputStream bout = null;
      try {
        bout = new ByteArrayOutputStream(64);
        int j = 0;
        int blockSize = cipher.getBlockSize();
        while (raw.length - j * blockSize > 0) {
          bout.write(cipher.doFinal(raw, j * blockSize, blockSize));
           j++;
        }
        return bout.toByteArray();
      } //后面是catch和finally,对bout 的安全处理,就不贴了
    }

    这样看貌似第二个安全一点,不过我不是很懂为什么要写的这么复杂,测了下运行时间貌似差不多。求各位指导。

    这里用到加密算法最核心的类Cipher类,我觉得有必要大概了解一下它,它的加密和解密过程用到的都是doFinal()方法,至于是加密还是解密就取决于init时候的参数Cliper的MODE。然后如果没有我说的BouncyCastle API你就要用 Cipher.getInstance(keyFactory.getAlgorithm())来实例化Clipher了,会麻烦一点。

    到这里加密解密就已经完成了~~~然后接下来可能会遇到一些问题,请看↓

    六、遇到问题了吧,说好的售后服务呢?

    (1)首先再提一下setMaxDigits(),要注意里面的参数,1024位对应130,2048位对应260。

    (2)然后是java里面关于解密时参数发送异常的问题:如果完全按照上述方法,或者是网上资料里面的做法,在大量使用的情况下就会出现以下报错。

    如果你是用精简版的RSAdecrypt

    org.bouncycastle.crypto.DataLengthException: input too large for RSA cipher.
    at org.bouncycastle.crypto.engines.RSACoreEngine.convertInput(Unknown Source)
    at org.bouncycastle.crypto.engines.RSABlindedEngine.processBlock(Unknown Source)
    at org.bouncycastle.jcajce.provider.asymmetric.rsa.CipherSpi.engineDoFinal(Unknown Source)
    at javax.crypto.Cipher.doFinal(Cipher.java:2087)

    如果你是用复杂版的RSAdecrypt

    java.lang.IllegalArgumentException: Bad arguments
     at javax.crypto.Cipher.doFinal(Cipher.java:2141)
     at com.dimeng.p2p.yylh.util.SecurityHelper.RSAdecrypt(SecurityHelper.java:236)
     at com.dimeng.p2p.yylh.util.SecurityHelper.getdecryptStr(SecurityHelper.java:262)
     at com.dimeng.p2p.yylh.util.SecurityHelper.main(SecurityHelper.java:342)
    Exception in thread "main" java.lang.IllegalArgumentException: Bad arguments
     at javax.crypto.Cipher.doFinal(Cipher.java:2141)

    原因是网上资料里面的用法是这样的,问题就出在toByteArray()上面

    //result是字符串类型的密文
    byte[] en_result = new BigInteger(result, 16).toByteArray();  
    byte[] de_result = RSAUtil.decrypt(RSAUtil.getKeyPair().getPrivate(),en_result);  

    准确来说是因为js加密的时候会导致byte[]类型密文比指定的长,为什么呢?因为上面提到的三个JS在加密密码时,偶尔会得出正确的密文byte[]多出一byte,里面是0,不信等报错了你自己试试。解决方法如下:

    /** * 16进制 To byte[] * @param hexString * @return byte[] */
    public static byte[] hexStringToBytes(String hexString) {
      if (hexString == null || hexString.equals("")) {
        return null;
      }
      hexString = hexString.toUpperCase();
      int length = hexString.length() / 2;
      char[] hexChars = hexString.toCharArray();
      byte[] d = new byte[length];
      for (int i = 0; i < length; i++) {
        int pos = i * 2;
        d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
      }
      return d;
    }
    
    /** * Convert char to byte * @param c char * @return byte */
    private static byte charToByte(char c) {
      return (byte) "0123456789ABCDEF".indexOf(c);
    }
    //这样~就对了
    byte[] en_result = hexStringToBytes(password_en);

    至此出错问题就能解决了

  • 相关阅读:
    10 种保护 Spring Boot 应用的绝佳方法
    Redis 如何分析慢查询操作?
    Spring Boot 主类及目录结构介绍
    Redis 再牛逼,也得设置密码!!
    Spring Data Redis 详解及实战一文搞定
    Spring Boot Redis Cluster 实战干货
    超详细的 Redis Cluster 官方集群搭建指南
    Redis Linux 安装运行实战全记录
    hdu 4790 Just Random (思路+分类计算+数学)
    poj 1328 Radar Installation(贪心)
  • 原文地址:https://www.cnblogs.com/henuyuxiang/p/6829812.html
Copyright © 2020-2023  润新知