Springboot+Shiro優雅實戰•加解密_2

一、背景

  1. 加解密是程序猿無法繞過的必備技能,但不少人都對加解密存在誤解:比如經常會有人把MD5這種Hash算法也當成加密算法;
  2. 加解密算法衆多,但是我們實際應用的卻只有那麼2-3種,下面着重講下對稱加密算法和非對稱加密算法,以及對應的業務場景;

二、目標

  1. 藉助第三方基礎庫(BCP),完成常用的對稱加密算法AES256和非對稱加密算法RSA1024的開發及驗證;
  2. 分析關鍵代碼和業務場景,加深理解;

三、步驟

  1. Maven配置文件pom.xml中引入BCP依賴(完整源碼見woollay/springboot-shiro):
<!-- 加密組件包 -->
<dependency>
   <groupId>org.bouncycastle</groupId>
   <artifactId>bcprov-jdk15on</artifactId>
   <version>1.62</version>
</dependency>
  1. 編寫AES256加解密代碼:
package com.justinsoft.encrypt;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.SecureRandom;
import java.security.Security;

/**
 * AES加解密算法
 * @since: JDK 1.8
 */
public final class AESEncrypt
{
    /**
     * 加密
     *
     * @param data   待加密數據
     * @param aesKey AES祕鑰
     * @return
     */
    public static byte[] encrypt(byte[] data, byte[] aesKey)
    {
        return doCipher(data, aesKey, Cipher.ENCRYPT_MODE);
    }

    /**
     * 解密
     *
     * @param data   加密數據
     * @param aesKey AES祕鑰
     * @return
     */
    public static byte[] decrypt(byte[] data, byte[] aesKey)
    {
        return doCipher(data, aesKey, Cipher.DECRYPT_MODE);
    }

    /**
     * 生成祕鑰
     *
     * @param initKey 初始key值,不完全使用系統的隨機函數
     * @return
     * @throws Exception
     */
    public static byte[] createKey(byte[] initKey) throws Exception
    {
        //1.添加BC算法支持
        //Security.addProvider(new BouncyCastleProvider());

        //2.使用BC算法生成祕鑰(添加用戶的初始祕鑰,不完全使用系統的隨機生成祕鑰的方法,可以避免系統漏洞)
        KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM, BC_PROVIDER);
        SecureRandom secureRandom = SecureRandom.getInstance(RANDOM_ALGORITHM);
        secureRandom.setSeed(initKey);

        //3.祕鑰的長度爲256(32字節)
        keyGenerator.init(ALGORITHM_LEN, secureRandom);
        SecretKey secretKey = keyGenerator.generateKey();
        return secretKey.getEncoded();
    }

    static
    {
        //1.添加BC算法支持
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * 轉換aesKey爲SecretKey對象
     *
     * @param aesKey
     * @return
     */
    private static SecretKey toKey(byte[] aesKey)
    {
        SecretKey key = new SecretKeySpec(aesKey, PADDING_ALGORITHM);
        return key;
    }

    /**
     * 加解密
     *
     * @param data       密文/明文
     * @param aesKey     AES祕鑰
     * @param cipherMode 算法模式:加密{@link Cipher#ENCRYPT_MODE},加密{@link Cipher#DECRYPT_MODE}
     * @return
     */
    private static byte[] doCipher(byte[] data, byte[] aesKey, int cipherMode)
    {
        try
        {
            //1.獲取AES Key對象
            Key key = toKey(aesKey);

            //2.使用BC填充算法
            Cipher cipher = Cipher.getInstance(PADDING_ALGORITHM, BC_PROVIDER);

            //3.初始化,設置爲加密/解密模式
            cipher.init(cipherMode, key);

            //4.加密/解密
            return cipher.doFinal(data);
        }
        catch (Exception e)
        {
            LOGGER.error("Failed to encrypt/decrypt data.", e);
            throw new RuntimeException("Failed to encrypt/decrypt data.");
        }
    }

    /**
     * 私有化構造方法
     */
    private AESEncrypt()
    {
    }

    //日誌
    private static final Logger LOGGER = LogManager.getLogger(AESEncrypt.class);

    /**
     * 密鑰算法
     */
    private static final String ALGORITHM = "AES";

    /**
     * 加密算法的長度 AES256
     */
    private static final int ALGORITHM_LEN = 256;

    /**
     * 使用BC作爲算法提供者
     */
    private static final String BC_PROVIDER = BouncyCastleProvider.PROVIDER_NAME;

    /**
     * 加密/解密算法的填充方式
     * <p>
     * JAVA6支持PKCS5Padding填充方式
     * BC支持PKCS7Padding填充方式
     */
    private static final String PADDING_ALGORITHM = "AES/ECB/PKCS7Padding";

    /**
     * 隨機算法
     */
    private static final String RANDOM_ALGORITHM = "SHA1PRNG";
}
  1. AES256算法是對稱加密算法,對稱加密算法就是說加密和解密的密鑰對是相同的,即:用什麼祕鑰加密,就得用該祕鑰去解密。

特點:加解密效率比較高。
場景:需要頻繁加解密的業務場景。比如:數據庫登錄密碼的加密。

  1. AES256的驗證UT代碼:
package com.justinsoft.encrypt;

import org.apache.commons.codec.Charsets;
import org.junit.Test;

import java.util.Arrays;

import static org.junit.Assert.assertTrue;

public class AESEncryptTest
{
    @Test
    public void createKey() throws Exception
    {
        String initKeyFactor = "com.justinsoft";
        byte[] secretKey = AESEncrypt.createKey(initKeyFactor.getBytes(Charsets.UTF_8));
        //1.生成祕鑰
        System.out.println("secretKey=" + Arrays.toString(secretKey));
        assertTrue(secretKey.length == 32);
    }

    @Test
    public void encrypt()
    {
        // 驗證明文字符串長度較短的加密情況:
        // 明文長度58byte,每16byte爲一個分組,密文應該是分成4組,每組16字節,所以密文一共64字節
        String testText1 = "1234567890987654321abcdefghijklmnopqrstuvwxyz~!@#$%^&*()_+";
        byte[] testByte1 = testText1.getBytes(Charsets.UTF_8);
        byte[] encryptText1 = AESEncrypt.encrypt(testByte1, EncryptKey.AES_KEY);
        System.out.println("srcLen1=" + testByte1.length + ",encryptText1=" + Arrays.toString(encryptText1) + ",len="
            + encryptText1.length);
        assertTrue(encryptText1.length == 64);

        // 驗證明文字符串長度較長的加密情況:
        // 明文長度65byte,每16byte爲一個分組,密文應該是分成5組,每組16字節,所以密文一共80字節
        String testText2 = "1234567890987654321abcdefghijklmnopqrstuvwxyz~!@#$%^&*()_+!@#$%^h";
        byte[] testByte2 = testText2.getBytes(Charsets.UTF_8);
        byte[] encryptText2 = AESEncrypt.encrypt(testByte2, EncryptKey.AES_KEY);
        System.out.println("srcLen2=" + testByte2.length + ",encryptText2=" + Arrays.toString(encryptText2) + ",len="
            + encryptText2.length);
        assertTrue(encryptText2.length == 80);
    }

    @Test
    public void decrypt()
    {
        // 驗證明文字符串長度較短的加密情況:
        // 明文長度58byte,每16byte爲一個分組,密文應該是分成4組,每組16字節,所以密文一共64字節
        String testText1 = "1234567890987654321abcdefghijklmnopqrstuvwxyz~!@#$%^&*()_+";
        byte[] testByte1 = testText1.getBytes(Charsets.UTF_8);
        byte[] encryptText1 = AESEncrypt.encrypt(testByte1, EncryptKey.AES_KEY);
        System.out.println("srcLen1=" + testByte1.length + ",encryptText1=" + Arrays.toString(encryptText1) + ",len="
            + encryptText1.length);
        assertTrue(encryptText1.length == 64);
        byte[] decryptByte1 = AESEncrypt.decrypt(encryptText1,EncryptKey.AES_KEY);
        assertTrue(decryptByte1.length == 58);
        String decryptText1 = new String(decryptByte1);
        assertTrue(decryptText1.equals(testText1));


        // 驗證明文字符串長度較長的加密情況:
        // 明文長度65byte,每16byte爲一個分組,密文應該是分成5組,每組16字節,所以密文一共80字節
        String testText2 = "1234567890987654321abcdefghijklmnopqrstuvwxyz~!@#$%^&*()_+!@#$%^h";
        byte[] testByte2 = testText2.getBytes(Charsets.UTF_8);
        byte[] encryptText2 = AESEncrypt.encrypt(testByte2, EncryptKey.AES_KEY);
        System.out.println("srcLen2=" + testByte2.length + ",encryptText2=" + Arrays.toString(encryptText2) + ",len="
            + encryptText2.length);
        assertTrue(encryptText2.length == 80);
        byte[] decryptByte2 = AESEncrypt.decrypt(encryptText2,EncryptKey.AES_KEY);
        assertTrue(decryptByte2.length == 65);
        String decryptText2 = new String(decryptByte2);
        assertTrue(decryptText2.equals(testText2));
    }
}
  1. AES256異常處理。在運行過程中,有概率會出現如下異常:

java.security.InvalidKeyException: Illegal key size or default parameters
該問題的解決方案見網友總結的博客。簡單來說安裝版的jdk6/jdk7/jdk8中默認只支持AES128,如果要支持AES256,需要升級官方補丁。一般解壓版可以直接支持AES256。

  1. 編寫RSA1024加解密代碼:
package com.justinsoft.encrypt;

import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * RSA非對稱加密算法
 * <br>
 * @since: JDK 1.8
 */
public final class RSAEncrypt
{
    /**
     * 加密,支持公鑰和私鑰
     *
     * @param data
     * @param key
     * @return
     */
    public static byte[] encrypt(byte[] data, byte[] key)
    {
        try
        {
            Key curKey = getKey(key);
            return doCipher(curKey, data, Cipher.ENCRYPT_MODE, MAX_ENCRYPT_BLOCK);
        }
        catch (Exception e)
        {
            LOGGER.error("Failed to encrypt data.", e);
            throw new RuntimeException("Failed to encrypt data.");
        }
    }

    /**
     * 解密,支持公鑰和私鑰
     *
     * @param data
     * @param key
     * @return
     */
    public static byte[] decrypt(byte[] data, byte[] key)
    {
        try
        {
            Key curKey = getKey(key);
            return doCipher(curKey, data, Cipher.DECRYPT_MODE, MAX_DECRYPT_BLOCK);
        }
        catch (Exception e)
        {
            LOGGER.error("Failed to decrypt data.", e);
            throw new RuntimeException("Failed to decrypt data.");
        }
    }

    /**
     * 獲取簽名
     *
     * @param data
     * @param privateKey
     * @return
     */
    public static byte[] sign(byte[] data, byte[] privateKey)
    {
        try
        {
            PrivateKey key = getPrivateKey(privateKey);
            Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
            signature.initSign(key);
            signature.update(data);
            return signature.sign();
        }
        catch (Exception e)
        {
            LOGGER.error("Failed to sign data.", e);
            throw new RuntimeException("Failed to sign data.");
        }
    }

    /**
     * 校驗數字簽名
     *
     * @param data
     * @param publicKey
     * @param sign
     * @return
     */
    public static boolean verify(byte[] data, byte[] publicKey, byte[] sign)
    {
        try
        {
            PublicKey key = getPublicKey(publicKey);
            Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
            signature.initVerify(key);
            signature.update(data);
            return signature.verify(sign);
        }
        catch (Exception e)
        {
            LOGGER.error("Failed to verify signature data.", e);
            throw new RuntimeException("Failed to verify signature data.");
        }
    }

    /**
     * 創建KeyPair
     * <br>
     * 目的是生成公鑰和私鑰
     *
     * @param initKey
     * @return
     * @throws Exception
     */
    public static KeyPair createKey(byte[] initKey) throws Exception
    {
        KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance(ALGORITHM, BC_PROVIDER);

        SecureRandom secureRandom = SecureRandom.getInstance(RANDOM_ALGORITHM);
        secureRandom.setSeed(initKey);

        keyGenerator.initialize(ALGORITHM_LEN, secureRandom);
        return keyGenerator.generateKeyPair();
    }

    static
    {
        //1.添加BC算法支持
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * 獲取key對象(公鑰)
     *
     * @param key
     * @return
     * @throws Exception
     */
    private static PublicKey getPublicKey(byte[] key) throws Exception
    {
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(key);
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        PublicKey publicKey = keyFactory.generatePublic(keySpec);
        return publicKey;
    }

    /**
     * 獲取key對象(私鑰)
     *
     * @param key
     * @return
     * @throws Exception
     */
    private static PrivateKey getPrivateKey(byte[] key) throws Exception
    {
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key);
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
        return privateKey;
    }

    /**
     * 加解密
     * 1.私鑰加密,公鑰解密;
     * 2.公鑰加密,私鑰解密;
     *
     * @param key        私鑰/公鑰
     * @param data       密文/明文
     * @param cipherMode 算法模式:加密{@link Cipher#ENCRYPT_MODE},加密{@link Cipher#DECRYPT_MODE}
     * @param maxLen     最大長度
     * @return
     */
    private static byte[] doCipher(Key key, byte[] data, int cipherMode, int maxLen) throws Exception
    {
        //1.使用BC填充算法
        Cipher cipher = Cipher.getInstance(PADDING_ALGORITHM, BC_PROVIDER);
        //2.初始化,設置爲加密/解密模式
        cipher.init(cipherMode, key);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int start = 0;
        try
        {
            while (start < data.length)
            {
                //3. 判定一次加解密的最大長度,不能超過data的總長度
                int limit = start + maxLen;
                limit = Math.min(limit, data.length);

                //4.分段加解密,並寫入字節流
                byte[] cacheByte = cipher.doFinal(data, start, limit - start);
                out.write(cacheByte, 0, cacheByte.length);

                //5.把起始位置移至上一次的結束位置
                start = limit;
            }
            //6.把字節流中的字節全部獲取出來
            byte[] resultData = out.toByteArray();
            return resultData;
        }
        finally
        {
            IOUtils.closeQuietly(out);
        }
    }

    /**
     * 獲取key
     *
     * @param keyByte
     * @return
     * @throws Exception
     */
    private static Key getKey(byte[] keyByte) throws Exception
    {
        Key key;
        if (keyByte.length == EncryptKey.RSA_PRI_KEY.length)
        {
            key = getPrivateKey(keyByte);
        }
        else
        {
            key = getPublicKey(keyByte);
        }
        return key;
    }

    /**
     * 私有化構造方法
     */
    private RSAEncrypt()
    {
    }

    //日誌
    private static final Logger LOGGER = LogManager.getLogger(RSAEncrypt.class);

    /**
     * 加密算法
     */
    private static final String ALGORITHM = "RSA";

    /**
     * 簽名算法
     */
    private static final String SIGNATURE_ALGORITHM = "SHA512withRSA";

    /**
     * 加密算法的長度 RSA1024
     */
    private static final int ALGORITHM_LEN = 1024;

    /**
     * RSA 1024最大的明文長度(非固定值)
     * 計算公式:
     * 1.1024bit=128byte
     * 2.128byte-11byte(PKCS1填充算法填充位)=117byte
     */
    private static final int MAX_ENCRYPT_BLOCK = 117;

    /**
     * RSA 1024最大的密文長度(固定值)
     * 計算公式:
     * 1.1024bit=128byte
     */
    private static final int MAX_DECRYPT_BLOCK = 128;

    /**
     * 使用BC作爲算法提供者
     */
    private static final String BC_PROVIDER = BouncyCastleProvider.PROVIDER_NAME;

    /**
     * 加密/解密算法的填充算法
     * <p>
     */
    private static final String PADDING_ALGORITHM = "RSA/ECB/PKCS1Padding";

    /**
     * 隨機算法
     */
    private static final String RANDOM_ALGORITHM = "SHA1PRNG";
}
  1. RSA1024算法是非對稱加密算法,就是說加密和解密的密鑰對是不同的。

特點:相比對稱加密算法來說更安全,因爲加解密祕鑰完全分離,加密方無須告知解密方自己的祕鑰;解密方也無須告知加密方自己的祕鑰。加密的效率較低。
場景:簽名、證書等。如:https裏面就使用的是RSA加密算法生成的證書;github上的ssh公私鑰也是RSA加密算法生成的。

  1. RSA1024的驗證UT代碼:
package com.justinsoft.encrypt;

import org.apache.commons.codec.Charsets;
import org.junit.Test;

import java.security.KeyPair;
import java.util.Arrays;

import static org.junit.Assert.assertTrue;

public class RSAEncryptTest
{
    @Test
    public void createKey() throws Exception
    {
        String initKeyFactor = "com.justinsoft";
        KeyPair keyPair = RSAEncrypt.createKey(initKeyFactor.getBytes(Charsets.UTF_8));
        //1.生成公祕鑰
        byte[] privateKey = keyPair.getPrivate().getEncoded();
        byte[] publicKey = keyPair.getPublic().getEncoded();
        System.out.println("privateKey=" + Arrays.toString(privateKey) + ",len=" + privateKey.length);
        System.out.println("publicKey=" + Arrays.toString(publicKey) + ",len=" + publicKey.length);
        assertTrue(privateKey.length > 12 && privateKey.length <= 1024);
        assertTrue(publicKey.length > 12 && publicKey.length < privateKey.length);
    }

    /**
     * 私鑰加密,公鑰解密
     */
    @Test
    public void decrypt()
    {
        String testText1 = "我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好";
        byte[] testByte1 = testText1.getBytes(Charsets.UTF_8);
        System.out.println("encryptByte1=" + Arrays.toString(testByte1) + ",len=" + testByte1.length);
        byte[] encryptByte1= RSAEncrypt.encrypt(testByte1,EncryptKey.RSA_PRI_KEY);
        System.out.println("encryptText1=" + Arrays.toString(encryptByte1) + ",len=" + encryptByte1.length);
        byte[] decryptByte1 = RSAEncrypt.decrypt(encryptByte1,EncryptKey.RSA_PUB_KEY);
        System.out.println("decryptText1=" + Arrays.toString(encryptByte1) + ",len=" + encryptByte1.length);
        //1、私鑰加密公鑰解密,驗證117字節的單次RSA加密
        assertTrue(testText1.equals(new String(decryptByte1)));
    }

    /**
     * 公鑰加密,私鑰解密
     */
    @Test
    public void decrypt2()
    {
        String testText1 = "我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好";
        byte[] testByte1 = testText1.getBytes(Charsets.UTF_8);
        System.out.println("encryptByte2=" + Arrays.toString(testByte1) + ",len=" + testByte1.length);
        byte[] encryptByte1= RSAEncrypt.encrypt(testByte1,EncryptKey.RSA_PUB_KEY);
        System.out.println("encryptText2=" + Arrays.toString(encryptByte1) + ",len=" + encryptByte1.length);
        byte[] decryptByte1 = RSAEncrypt.decrypt(encryptByte1,EncryptKey.RSA_PRI_KEY);
        System.out.println("decryptText2=" + Arrays.toString(encryptByte1) + ",len=" + encryptByte1.length);
        //1、公鑰加密,私鑰解密,驗證117字節明文的單次RSA加密
        assertTrue(testText1.equals(new String(decryptByte1)));
    }

    @Test
    public void decrypt3()
    {
        String testText1 = "我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好孩";
        byte[] testByte1 = testText1.getBytes(Charsets.UTF_8);
        System.out.println("encryptByte3=" + Arrays.toString(testByte1) + ",len=" + testByte1.length);
        byte[] encryptByte1= RSAEncrypt.encrypt(testByte1,EncryptKey.RSA_PUB_KEY);
        System.out.println("encryptText3=" + Arrays.toString(encryptByte1) + ",len=" + encryptByte1.length);
        assertTrue(encryptByte1.length==256);
        byte[] decryptByte1 = RSAEncrypt.decrypt(encryptByte1,EncryptKey.RSA_PRI_KEY);
        System.out.println("decryptText3=" + Arrays.toString(encryptByte1) + ",len=" + encryptByte1.length);
        //1、公鑰加密,私鑰解密,驗證120字節的多次循環RSA加密
        assertTrue(testText1.equals(new String(decryptByte1)));
    }

    @Test
    public void verify()
    {
        String testText1 = "我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好孩子啊好孩子!我們都是好孩";
        byte[] testByte1 = testText1.getBytes(Charsets.UTF_8);
        System.out.println("encryptByte4=" + Arrays.toString(testByte1) + ",len=" + testByte1.length);
        byte[] signByte = RSAEncrypt.sign(testByte1,EncryptKey.RSA_PRI_KEY);
        boolean isVerify = RSAEncrypt.verify(testByte1,EncryptKey.RSA_PUB_KEY,signByte);
        assertTrue(isVerify);
    }
}
  1. RSA1024算法需要注意的是一次只能加解密117個字節(原因見代碼中的註釋),所以,如果報文過長,需要進行分段加解密,核心代碼見RSA1024代碼。

四、總結

  1. AES256和RSA1024的使用方式差不多,都是要傳入要加密的內容和祕鑰,只不過AES256加解密的祕鑰是同一個,而RSA1024是不同的2個值,觀察完整的源碼就會發現公鑰和私鑰的長度都完全不同;RSA1024算法還可用於驗證簽名;需要特別強調的是:RSA1024每次加密同一份內容的密文也不相同;
  2. AES256用於數據庫登錄密碼或者部分C/S結構的客戶端密碼的加解密,效率也高;RSA1024主要用於https,github的ssh連接的加解密,加解密效率偏低;

五、參考

[1]開發中的幾種加密算法的使用場景
[2]AES的256位密鑰加解密報 java.security.InvalidKeyException: Illegal key size or default parameters 異常的處理及處理工具

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章