登录认证、鉴权这些都做好了过后。就开始我们的加密设计了、这里采用了简化数字信封进行加密。首先客户端(浏览器)先请求一份RSA非对称密钥、如果我们采用了openresty或者有能力在nginx开发C模块的插件,就可以在这里保留一份用户的私钥,如果不行就直接在应用网关上面保存(也可以在应用网关直接读取redis获得);然后在浏览器发起请求的时候、请求体加密时本地自己生成AES密钥、在使用获得的公钥对AES密钥进行一次加密(加密结果放在请求头中),最后将请求发送到后端。后端先使用RSA的私钥解密请求头中的AES加密密钥,得到了AES明文密钥后对请求体进行解密。后端返回时,如果需要加密返回结果、也可以使用该AES密钥对结果进行加密。下图以有nginx自定义C模块或者openresty进行加解密的方式进行描述。
AES加密
采用AES进行对称加密时、需要注意的是需要采用CBC模式。CBC模式会增加向量来保证加密的强度。还有为什么明明使用了RSA还需要在用AES做什么呢?为什么不全部使用RSA呢?
首先RSA的强度是要比AES更强的、但是RSA有一个缺陷就是密文太长的话、解密的速度太慢。所以综合了两者的优点、使用RSA对AES的密钥加密,使用AES对报文体进行加密。这样既保证了密码的强度、又保证了加解密的速度问题。
AES加解密工具类
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
/**
* AES 加密工具类
*/
public class AESUtils {
public static final String CHAR_ENCODING = "UTF-8";
public static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";
/**
* 加密
*
* @param data 需要加密的内容
* @param key 加密密码
* @return
*/
public static byte[] encrypt(byte[] data, byte[] key, byte[] ivKey) {
if (key.length != 16) {
throw new RuntimeException("Invalid AES key length (must be 16 bytes)");
}
try {
SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec seckey = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);// 创建密码器
IvParameterSpec iv = new IvParameterSpec(ivKey);//使用CBC模式,需要一个向量iv,可增加加密算法的强度
cipher.init(Cipher.ENCRYPT_MODE, seckey, iv);// 初始化
byte[] result = cipher.doFinal(data);
return result; // 加密
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("encrypt fail!", e);
}
}
/**
* 解密
*
* @param data 待解密内容
* @param key 解密密钥
* @return
*/
public static byte[] decrypt(byte[] data, byte[] key,byte[] ivKey) {
if (key.length != 16) {
throw new RuntimeException("Invalid AES key length (must be 16 bytes)");
}
try {
SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec seckey = new SecretKeySpec(enCodeFormat, "AES");
// 创建密码器
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
// 使用CBC模式,需要一个向量iv,可增加加密算法的强度
IvParameterSpec iv = new IvParameterSpec(ivKey);
// 初始化
cipher.init(Cipher.DECRYPT_MODE, seckey, iv);
byte[] result = cipher.doFinal(data);
return result; // 解密
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("decrypt fail!", e);
}
}
public static String encryptToBase64(String data, String key, String iVkey) {
try {
byte[] valueByte = encrypt(data.getBytes(CHAR_ENCODING), key.getBytes(CHAR_ENCODING),iVkey.getBytes(CHAR_ENCODING));
return new String(Base64.encodeBase64(valueByte));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("encrypt fail!", e);
}
}
public static String decryptFromBase64(String data, String key, String iVkey) {
try {
byte[] originalData = Base64.decodeBase64(data.getBytes());
final char[] chars = getChars(decrypt(originalData, key.getBytes(CHAR_ENCODING),iVkey.getBytes(CHAR_ENCODING)));
StringBuffer sb = new StringBuffer();
for(int i= 0 ;i < chars.length; i++){
if(chars[i] == '\0'){
continue;
}
sb.append(chars[i]);
}
Arrays.fill(chars,' ');
return sb.toString();
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("decrypt fail!", e);
}
}
private static char[] getChars(byte[] bytes){
Charset cs = Charset.forName(CHAR_ENCODING);
ByteBuffer bb = ByteBuffer.allocate(bytes.length);
bb.put(bytes);
bb.flip();
CharBuffer cb = cs.decode(bb);
return cb.array();
}
public static byte[] genarateRandomKey() {
KeyGenerator keygen = null;
try {
keygen = KeyGenerator.getInstance(AES_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(" genarateRandomKey fail!", e);
}
/* SecureRandom random = new SecureRandom();
keygen.init(random);*/
keygen.init(128);
Key key = keygen.generateKey();
return key.getEncoded();
}
}
RSA加密
上面提到会在nginx那边存储一份私钥、那么如何保证客户端(浏览器)的每一对公私玥都是能对应上的呢。这里就需要在做一个设计、就是客户端在请求公钥之前先获取一个临时的token、临时token跟正式token的区别在于临时token的权限是固定的一些URI。并且临时token中的userId是随机生成的虚拟的,临时token的时效也不用设置太长。当用户登录成功后token中的tokenId(之前提到的使用雪花字符串或者UUID)不变、只需要替换里面的userId跟重新设置redis的有效时长即可。这样nginx就可以使用token作为关键字存储每一个客户端对应的私钥了。
RSA工具类
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.*;
import java.util.HashMap;
import java.util.Map;
/**
* RSA处理工具类
*
* @author hhs
* @date 2019-10-22 09:54
* @since JDK1.8
*/
@Slf4j
public class RsaPassUtils {
/**
* 填充模式
*/
private final static String RSA_CIPHER = "RSA/ECB/OAEPWithSHA-1AndMGF1PADDING";
/**
* 密钥位数
*/
private final int KEY_SIZE = 2048;
private KeyPair KEY_PAIR;
public RsaPassUtils(){
try {
SecureRandom random = new SecureRandom();
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(KEY_SIZE, random);
KEY_PAIR = generator.generateKeyPair();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 生成rsa公钥和私钥
*
* @return Map {@link HashMap}: publicKey-公钥(RSAPublicKey),privateKey-私钥(RSAPrivateKey)
* @author hhs
* @date 2019-10-22 09:54
*/
public Map<String, Object> getRsaKey() {
Map<String, Object> keyInfo = new HashMap<>(2);
// 公钥
RSAPublicKey publicKey = (RSAPublicKey) KEY_PAIR.getPublic();
keyInfo.put("publicKey", publicKey);
// 私钥
RSAPrivateKey privateKey = (RSAPrivateKey) KEY_PAIR.getPrivate();
keyInfo.put("privateKey", privateKey);
return keyInfo;
}
/**
* 生成rsa公钥和私钥
*
* @return Map {@link HashMap}: publicKey-公钥(X509格式),privateKey-私钥(PKCS8格式)
* @author hhs
* @date 2019-10-22 09:54
*/
public Map<String, String> getX509AndPKCS8Key() {
Map<String, Object> keyInfo = getRsaKey();
Map<String, String> x509AndPKCS8Key = new HashMap<>(2);
// 公钥
RSAPublicKey publicKey = (RSAPublicKey) keyInfo.get("publicKey");
// String rsaPublicKey = (new BASE64Encoder()).encodeBuffer(publicKey.getEncoded());
String rsaPublicKey = Base64.encodeBase64String(publicKey.getEncoded());
x509AndPKCS8Key.put("publicKey", rsaPublicKey);
// 私钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyInfo.get("privateKey");
// String rsaPrivateKey = (new BASE64Encoder()).encodeBuffer(privateKey.getEncoded());
String rsaPrivateKey = Base64.encodeBase64String(privateKey.getEncoded());
x509AndPKCS8Key.put("privateKey", rsaPrivateKey);
return x509AndPKCS8Key;
}
/**
* PKCS8的私钥字符串还原为RSA私钥
*
* @param pkcs8Key {@link String} 待还原私钥字符串
* @return RSAPrivateKey {@link RSAPrivateKey}
* @author hhs
* @date 2019-10-22 09:54
*/
private RSAPrivateKey getPrivateKey(String pkcs8Key) {
PrivateKey privateKey = null;
try {
// byte[] decodeKey = (new BASE64Decoder()).decodeBuffer(pkcs8Key);
byte[] decodeKey = Base64.decodeBase64(pkcs8Key);
PKCS8EncodedKeySpec pkcs8 = new PKCS8EncodedKeySpec(decodeKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
privateKey = keyFactory.generatePrivate(pkcs8);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
log.error("----------RSAUtils---------->PKCS8的私钥字符串还原为RSA私钥出错:{}", e.getMessage());
}
return (RSAPrivateKey) privateKey;
}
/**
* X509的公钥字符串还原为RSA公钥
*
* @param x509Key {@link String} 待还原公钥字符串
* @return RSAPublicKey {@link RSAPublicKey}
* @author hhs
* @date 2019-10-22 09:54
*/
private RSAPublicKey getPublicKey(String x509Key) {
PublicKey publicKey = null;
try {
// byte[] decodeKey = (new BASE64Decoder()).decodeBuffer(x509Key);
byte[] decodeKey = Base64.decodeBase64(x509Key);
X509EncodedKeySpec x509 = new X509EncodedKeySpec(decodeKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
publicKey = keyFactory.generatePublic(x509);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
log.error("----------RSAUtils---------->X509的公钥字符串还原为RSA公钥:{}", e.getMessage());
}
return (RSAPublicKey) publicKey;
}
/**
* <p>使用模和指数生成RSA公钥
* 注意:【此代码用了默认补位方式,为RSA/None/PKCS1Padding,不同JDK默认的补位方式可能不同,如Android默认是RSA/None/NoPadding】
* </p>
*
* @param modulus {@link BigInteger} 模
* @param publicExponent {@link BigInteger} 公钥指数
* @return RSAPublicKey {@link RSAPublicKey}
* @author hhs
* @date 2019-10-22 09:54
*/
public RSAPublicKey getPublicKey(BigInteger modulus, BigInteger publicExponent) {
RSAPublicKey publicKey = null;
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, publicExponent);
publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
log.info("----------RSAUtils---------->生成公钥:{}", publicKey);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
log.error("----------RSAUtils---------->生成公钥异常:{}", e.getMessage());
}
return publicKey;
}
/**
* <p>
* 使用模和指数生成RSA私钥
* 注意:【此代码用了默认补位方式,为RSA/None/PKCS1Padding,不同JDK默认的补位方式可能不同,如Android默认是RSA/None/NoPadding】
* </p>
*
* @param modulus {@link BigInteger} 模
* @param privateExponent {@link BigInteger} 私钥指数
* @return RSAPrivateKey {@link RSAPrivateKey}
* @author hhs
* @date 2019-10-22 09:54
*/
public RSAPrivateKey getPrivateKey(BigInteger modulus, BigInteger privateExponent) {
RSAPrivateKey privateKey = null;
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPrivateKeySpec keySpec = new RSAPrivateKeySpec(modulus, privateExponent);
privateKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
log.error("----------RSAUtils---------->生成私钥异常:{}", e.getMessage());
}
return privateKey;
}
/**
* 公钥加密
*
* @param publicKey {@link String} X509格式公钥
* @param plaintext {@link String} 明文
* @return String {@link String} 密文
* @author hhs
* @date 2019-10-22 09:54
*/
public String encryptPublicKey(String publicKey, String plaintext) {
RSAPublicKey rsaPublicKey = getPublicKey(publicKey);
String result = "";
try {
byte[] bytes = encryptByRsaKey(rsaPublicKey, plaintext.getBytes());
result = Base64.encodeBase64String(bytes);
} catch (Exception e) {
log.error("----------RSAUtils---------->公钥加密异常:{}", e.getMessage());
}
return result;
}
/**
* 公钥解密
*
* @param publicKey {@link String} X509格式公钥
* @param ciphertext {@link String} 密文
* @return String {@link String} 明文
* @author hhs
* @date 2019-10-22 09:54
*/
public String decryptPublicKey(String publicKey, String ciphertext) {
byte[] bytes = Base64.decodeBase64(ciphertext);
RSAPublicKey rsaPublicKey = getPublicKey(publicKey);
String result = "";
try {
result = new String(decryptByRsaKey(rsaPublicKey, bytes), StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("----------RSAUtils---------->私钥解密异常:{}", e.getMessage());
}
return result;
}
/**
* 私钥加密
*
* @param privateKey {@link String} PKCS8格式私钥
* @param plaintext {@link String} 明文
* @return String {@link String} 密文
* @author hhs
* @date 2019-10-22 09:54
*/
public String encryptPrivateKey(String privateKey, String plaintext) {
RSAPrivateKey rsaPrivateKey = getPrivateKey(privateKey);
String result = "";
try {
byte[] bytes = encryptByRsaKey(rsaPrivateKey, plaintext.getBytes());
result = Base64.encodeBase64String(bytes);
} catch (Exception e) {
log.error("----------RSAUtils---------->公钥加密异常:{}", e.getMessage());
}
return result;
}
/**
* 私钥解密
*
* @param privateKey {@link String} PKCS8格式私钥
* @param ciphertext {@link String} 密文
* @return String {@link String}
* @author hhs
* @date 2019-10-22 09:54
*/
public String decryptPrivateKey(String privateKey, String ciphertext) {
byte[] bytes = Base64.decodeBase64(ciphertext);
RSAPrivateKey rsaPrivateKey = getPrivateKey(privateKey);
String result = "";
try {
result = new String(decryptByRsaKey(rsaPrivateKey, bytes), StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("----------RSAUtils---------->私钥解密异常:{}", e.getMessage());
}
return result;
}
/**
* 数据分组解密
*
* @param key {@link Key} RSA公钥或者私钥
* @param ciphertext-密文byte数组
* @return byte[] 明文byte数组
* @throws Exception {@link Exception} 异常数据
* @author hhs
* @date 2019-10-22 09:54
*/
private byte[] decryptByRsaKey(Key key, byte[] ciphertext) throws Exception {
// Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding");
Cipher cipher = Cipher.getInstance(RSA_CIPHER);
cipher.init(2, key);
int inputLen = ciphertext.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
for (int i = 0; inputLen - offSet > 0; offSet = i * 256) {
byte[] cache;
if (inputLen - offSet > 256) {
cache = cipher.doFinal(ciphertext, offSet, 256);
} else {
cache = cipher.doFinal(ciphertext, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
++i;
}
byte[] decryptedData = out.toByteArray();
out.close();
return decryptedData;
}
/**
* 数据分组加密
*
* @param key {@link Key} Rsa公钥或私钥
* @param plaintext-明文byte数组
* @return byte[] 密文byte数组
* @throws Exception {@link Exception} 异常数据
* @author hhs
* @date 2019-10-22 09:54
*/
private byte[] encryptByRsaKey(Key key, byte[] plaintext) throws Exception {
// Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding");
Cipher cipher = Cipher.getInstance(RSA_CIPHER);
cipher.init(1, key);
int inputLen = plaintext.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
for (int i = 0; inputLen - offSet > 0; offSet = i * 244) {
byte[] cache;
if (inputLen - offSet > 244) {
cache = cipher.doFinal(plaintext, offSet, 244);
} else {
cache = cipher.doFinal(plaintext, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
++i;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptedData;
}
}
注意在填充时、不要在构造中添加"BC"参数,该模式配合到C语言进行解密时,C语言不好处理、所以需要将该参数去掉:
Cipher cipher = Cipher.getInstance(RSA_CIPHER,
"BC");