最近在做一个用户 token 功能,学习了加密相关 AES/DES、RSA 等。其中涉及一个对称和非对称加密问题。对称加密虽然没有非对称加密那样安全性高,但好处是加密速度快,但某些场合还是可以选择使用的,例如当下的用户认知机制,它是基于 token 无状态的,每次请求过来都会认证一次,这样就必须要比较高速度的加密解密运算,于是我们选择了 AES 加密方式。
本包提供 DES/AES 对称加密/解密和 RSA 非对称加密/解密功能,所在的包是 com.ajaxjs.util.cryptography。
对称加密虽然没有非对称加密那样安全性高,但好处是加密速度快,但某些场合还是可以选择使用的,例如当下的用户认知机制,它是基于 token 无状态的,每次请求过来都会认证一次,这样就必须要比较高速度的加密解密运算,这时选择 AES 就比较合适了。
AES 对称加密用法如下。
String input = "cy11Xlbrmzyh:604:301:1353064296";
String key = "37d5aed075525d4fa0fe635231cba447";
String EncryptedPassword = SymmetricCipher.AES_Encrypt(input, key);
assertEquals(input, SymmetricCipher.AES_Decrypt(EncryptedPassword, key));
RAS 非对称加密用法如下。
String input = "美好的一天";
Map<String, byte[]> keyMap = generateKeyBytes();
// 加密
PublicKey publicKey = restorePublicKey(keyMap.get(PUBLIC_KEY));
byte[] encodedText = RSAEncode(publicKey, input.getBytes());
// 解密
PrivateKey privateKey = restorePrivateKey(keyMap.get(PRIVATE_KEY));
String decoded = RSADecode(privateKey, encodedText);
assertEquals(input, decoded);
下面是对称加密的工具类,完全可以不依赖其他三方 jar 包。不过有个 base64 方法我封装起来了(下面会补充)。
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import com.ajaxjs.util.Encode;
/**
* 对称算法
* @author admin
*
*/
public class SymmetricCipher {
public SymmetricCipher() {}
public SymmetricCipher(String ALGORITHM, int keySize) {
this.ALGORITHM = ALGORITHM;
this.keySize = keySize;
}
/**
* DES = 56 | AES = 128
*/
private int keySize = 128;
/**
* 加密算法,可以 DES | AES
*/
private String ALGORITHM = "AES";
/**
* 加密
*
* @param str
* 要加密的内容
* @param key
* 密钥
* @return 加密后的内容
*/
public String encrypt(String str, String key) {
// 这里要设置为utf-8不然内容中如果有中文和英文混合中文就会解密为乱码
return doCipher(true, key, str, str.getBytes(StandardCharsets.UTF_8));
}
/**
* 解密
*
* @param str
* 要解密的内容
* @param key
* 密钥
* @return 解密后的内容
*/
public String decrypt(String str, String key) {
return doCipher(false, key, str, Encode.base64DecodeAsByte(str));
}
private String doCipher(boolean isENCRYPT_MODE, String key, String str, byte[] bytes) {
Cipher cipher = null;
try {
cipher = Cipher.getInstance(ALGORITHM);
cipher.init(isENCRYPT_MODE ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, generateKey(key));
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException e) {
e.printStackTrace();
return null;
}
byte[] buf;
try {
// 为了防止解密时报javax.crypto.IllegalBlockSizeException: Input length must
// be multiple of 8 when decrypting with padded cipher异常,
// 不能把加密后的字节数组直接转换成字符串
buf = cipher.doFinal(bytes);
} catch (IllegalBlockSizeException | BadPaddingException e) {
e.printStackTrace();
return null;
}
return isENCRYPT_MODE ? Encode.base64Encode(buf) : Encode.byte2String(buf);
}
/**
* 获得密钥对象
*
* @param key
* 密钥
* @return 密钥对象
*/
private SecretKey generateKey(String key) {
SecureRandom secureRandom;
KeyGenerator kg;
try {
secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(key.getBytes());
kg = KeyGenerator.getInstance(ALGORITHM);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
kg.init(keySize, secureRandom);
return kg.generateKey();// 生成密钥
}
final static SymmetricCipher AES = new SymmetricCipher();
final static SymmetricCipher DES = new SymmetricCipher("DES", 56);
/**
* AES 加密
*
* @param str
* 要加密的内容
* @param key
* 密钥
* @return 加密后的内容
*/
public static String AES_Encrypt(String str, String key) {
return AES.encrypt(str, key);
}
/**
* AES 解密
* @param str
* @param key
* @return
*/
public static String AES_Decrypt(String str, String key) {
return AES.decrypt(str, key);
}
}
使用方法如下
import org.junit.Test;
import static org.junit.Assert.*;
import com.ajaxjs.user.password.SymmetricCipher;
public class TestPassword {
String input = "cy11Xlbrmzyh:604:301:1353064296";
String key = "37d5aed075525d4fa0fe635231cba447";
@Test
public void testEncryption() {
String EncryptedPassword = SymmetricCipher.AES_Encrypt(input, key);
// System.out.println("EncryptedPassword::" +EncryptedPassword);
assertEquals(input, SymmetricCipher.AES_Decrypt(EncryptedPassword, key));
}
}
Base64 编码解码方法
编码工具类是 com.ajaxjs.util.Encode。
该类主要围绕字符串的 base64 编码/解码、URL 编码/解码和Java 字节类型 byte 转换这三种任务而生。用法如下:
static String str = "中国";
assertEquals("abc", byte2String(new byte[] {97, 98, 99}));
assertEquals("abc", byte2String("abc"));
assertEquals("中国", byte2String(str));
assertEquals(urlDecode(urlEncode(str)), str);
assertEquals(base64Deode(base64Encode(str)), str);
Base64 编码转换使用到了 Java 内部方法 sun.misc.*,如果不能够使用这个包,可以导入 org.apache.commons.codec.binary.Base64。
package com.ajaxjs.util;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
/**
* 字符串的编码、解密
* @author admin
*
*/
public class Encode {
/**
* 字节转编码为 字符串( UTF-8 编码)
*
* @param bytes
* 输入的字节数组
* @return 字符串
*/
public static String byte2String(byte[] bytes) {
return new String(bytes, StandardCharsets.UTF_8);
}
/**
* 字符串转为 UTF-8 编码的字符串
*
* @param str
* 输入的字符串
* @return UTF-8 字符串
*/
public static String byte2String(String str) {
return byte2String(str.getBytes());
}
/**
* 将 URL 编码的字符还原,默认 UTF-8 编码
*
* @param str
* 已 URL 编码的字符串
* @return 正常的 Java 字符串
*/
public static String urlDecode(String str) {
try {
return URLDecoder.decode(str, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
return null;
}
}
/**
* 将字符进行 URL 编码,默认 UTF-8 编码
*
* @param str
* 正常的 Java 字符串
*
* @return 已 URL 编码的字符串
*/
public static String urlEncode(String str) {
try {
return URLEncoder.encode(str, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
return null;
}
}
/**
* url 网址中文乱码处理。
* 如果 Tomcat 过滤器设置了 utf-8 那么这里就不用重复转码了
*
* @param str
* 通常是 url Query String 参数
* @return 中文
*/
public static String urlChinese(String str) {
return byte2String(str.getBytes(StandardCharsets.ISO_8859_1));
}
/**
* BASE64 编码
* @param bytes输入的字节数组
* @return 已编码的字符串
*/
public static String base64Encode(byte[] bytes) {
return new BASE64Encoder().encode(bytes);
}
/**
* BASE64 编码
*
* @param str
* 待编码的字符串
* @return 已编码的字符串
*/
public static String base64Encode(String str) {
return base64Encode(str.getBytes());
}
public static byte[] base64DecodeAsByte(String str) {
BASE64Decoder decoder = new BASE64Decoder();
try {
return decoder.decodeBuffer(str);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
/**
* BASE64 解码 这里需要强制捕获异常。
* 中文乱码:http://s.yanghao.org/program/viewdetail.php?i=54806
*
* @param str
* 已解码的字符串
* @return 已解码的字符串
*/
public static String base64Decode(String str) {
return byte2String(base64DecodeAsByte(str));
}
}
Hash 摘要算法
Hash函数又称杂凑函数,用于摘要算法,它把不定长的明文信息经过复杂的运算得到一个定长的数值,这就是具有独一无二特征的“签名”。摘要算法与一般的对称或非对称加密算法不同,它并不用于防止信息被窃取,而是用于证明原文的完整性和准确性,也就是说,数字签名主要是用于防止信息被篡改。
摘要算法工具类是 com.ajaxjs.util.Hash,有两个方法,一个是 md5 方法,另外一个 Sha1 方法。用法如下。
assertEquals(md5("123123"), "4297F44B13955235245B2497399D7A93");
assertEquals(getSHA1("abc"), "a9993e364706816aba3e25717850c26c9cd0d89d");
2020-4-3 更新
进一步封装了共用的地方,避免了代码重复,形成一个 CipherInfo 类,可在此基础上扩展 AES/DES/RSA 的方法。
package com.ajaxjs.util.cryptography;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import com.ajaxjs.util.logger.LogHelper;
/**
* 简单封装了 Java Cipher 类提供了加密和解密的功能。
*
* @author sp42 frank@ajaxjs.com
*
*/
public class CipherInfo {
private static final LogHelper LOGGER = LogHelper.getLog(CipherInfo.class);
/**
* 加密算法
*/
private String cipherAlgorithm;
/**
* 密钥长度
*/
private int keySize;
/**
* 创建一个 CipherInfo 实例
*
* @param cipherAlgorithm 加密算法
* @param keySize 密钥长度
*/
public CipherInfo(String cipherAlgorithm, int keySize) {
this.cipherAlgorithm = cipherAlgorithm;
this.keySize = keySize;
}
/**
* 进行加密或解密,三步走
*
* @param algorithm 选择的算法
* @param mode 是解密模式还是加密模式?
* @param key 密钥
* @param s 输入的内容
* @return 结果
*/
static byte[] doCipher(String algorithm, int mode, Key key, byte[] s) {
try {
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(mode, key);
/*
* 为了防止解密时报 javax.crypto.IllegalBlockSizeException: Input length must be
* multiple of 8 when decrypting with padded cipher 异常, 不能把加密后的字节数组直接转换成字符串
*/
return cipher.doFinal(s);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException
| BadPaddingException e) {
LOGGER.warning(e);
return null;
}
}
/**
* AES/DES 专用
*
* @param ci 密码信息
* @param mode 是解密模式还是加密模式?
* @param key 密钥
* @param bytes 输入的内容,可以是字符串转换 byte[]
* @return 转换后的内容
*/
static byte[] doCipher(CipherInfo ci, int mode, String key, byte[] bytes) {
SecretKey _key;// 获得密钥对象
try {
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
sr.setSeed(key.getBytes());
KeyGenerator kg = KeyGenerator.getInstance(ci.getCipherAlgorithm());
kg.init(ci.getKeySize(), sr);
_key = kg.generateKey();// 生成密钥
} catch (NoSuchAlgorithmException e) {
LOGGER.warning(e);
return null;
}
return doCipher(ci.getCipherAlgorithm(), mode, _key, bytes);
}
public int getKeySize() {
return keySize;
}
public void setKeySize(int keySize) {
this.keySize = keySize;
}
public String getCipherAlgorithm() {
return cipherAlgorithm;
}
public void setCipherAlgorithm(String cipherAlgorithm) {
this.cipherAlgorithm = cipherAlgorithm;
}
}
AES 应用例子如下
package com.ajaxjs.util.cryptography;
import java.nio.charset.StandardCharsets;
import javax.crypto.Cipher;
import com.ajaxjs.util.Encode;
/**
* AES 对称算法 SymmetricCipher
*
* @author sp42 frank@ajaxjs.com
*
*/
public class AES_Cipher {
private final static CipherInfo ci = new CipherInfo("AES", 128);
/**
* 加密
*
* @param str 要加密的内容
* @param key 密钥
* @return 加密后的内容
*/
public static String encrypt(String str, String key) {
// (这里要设置为 utf-8)不然内容中如果有中文和英文混合中文就会解密为乱码
return Encode
.base64Encode(CipherInfo.doCipher(ci, Cipher.ENCRYPT_MODE, key, str.getBytes(StandardCharsets.UTF_8)));
}
/**
* 解密
*
* @param str 要解密的内容
* @param key 密钥
* @return 解密后的内容
*/
public static String decrypt(String str, String key) {
return Encode.byte2String(CipherInfo.doCipher(ci, Cipher.DECRYPT_MODE, key, Encode.base64DecodeAsByte(str)));
}
}