文章目录
- 1、背景
- 2、需求
- 3、实现思路
- 3.1 密码加密
- 3.2 密码解密
- 3.3 nacos密码加密
- 4、相关工具类
- 4.1 非对称加密RSA
- 4.2 对称加密AES
- 4.3 Nacos加解密的实现:Jasypt
- 5、历史数据兼容处理
1、背景
用户在浏览器发送请求数据到后台系统,期间数据在网络传输,如果这些数据是敏感数据,被恶意拦截时,就有安全问题,造成用户密码泄漏等等。
如此,可考虑使用非对称加密或者对称加密,给前端一个公钥,让前端把数据用公钥加密后传到后端,后端负责解密得到原始数据后再处理请求,如此,即使被恶意拦截,也无法得到真实密码。
对称加密即:文件的加密和解密都是使用相同的密钥。加密方和解密方使用同一把钥匙。
对称加密的优点是加密速度快,缺点是相对不安全(如果别人知道你用的哪种加密算法且密钥泄漏,则一切形同虚设)。
非对称加密即:两个密钥,公钥用来加密,私钥用来解密
和对称加密相比,安全性更高,但加解密更慢,数据量小时可采用。对称加密和非对称加密这两种思想,市面上有多种不同的具体落地的算法。对称加密如AES,非对称加密如RSA。 具体选择:
- 文件很大建议使用对称加密
- 文件较小,要求安全性高,建议采用非对称加密
2、需求
对系统中敏感数据进行加密,保证数据安全。敏感数据包括:用户密码、用户手机号、用户邮箱、Nacos配置中各个中间件的密码。加密方向包括:
- 数据加密传输:前端调用后端接口时,先用公钥对密码进行加密,再使用base64编码,然后传输
- 数据加密存储:后端落库时,base64解码,再用私钥解密,对明文密码要经过Bcrypt等不可逆加密算法加密后保存,防止被拖库
3、实现思路
3.1 密码加密
这里使用非对称加密实现更合理。针对以上要加密的数据,用户密码处理的流程图如下:修改注册后端接口,用户注册时,提交密码,前端用公钥对密码进行加密后,传到后端服务器。后端接口中用私钥对密码进行解密,实现加密传输。解密后,再对解密后的明文密码进行加密,存入数据库,实现加密存储。项目中用到了Spring Security框架,所以这里用Bcrypt算法进行加密存储,Bcrypt也可防止彩虹表破解。
对邮箱名、手机号等信息,可非对称加密,也可使用AES对称加密,实现加密传输,加密落库则可有可无,如果选择了加密落库,可能会影响到之前的userList接口等等,总之明文、密文别转换叉了。
3.2 密码解密
修改后端登录接口,登录时,前端传来的密码,解密后传到SpringSecurity框架,如果账户是加密传输的,也需解密,因为框架里要loadUserByUsername,用户名得转换过来。
流程图:
3.3 nacos密码加密
项目中,用Nacos做配置管理,很多中间件,如MySQL、Redis的密码都明文存储在配置文件中,考虑改为密文存储。SpringBoot服务启动时,去Nacos拉取配置、注册服务信息。改为密文后,需要先解密,才能连接中间件成功,实现这个可以考虑加一个Filter过滤器或者AOP,在读配置文件时,判断如果是密文,则解密后重新赋值。这里直接用已有的开源实现:Jasypt
//官方文档:
https://github.com/ulisesbocchio/jasypt-spring-boot
//源码解析:
https://blog.csdn.net/u013905744/article/details/86508236
大致看了下,实现思路是借助SpringBoot Bean加载的扩展点,做一个过滤器,如果读到的内容是以Jasypt指定的前后缀ENC()
,则解密后重新赋值:
4、相关工具类
4.1 非对称加密RSA
加密和解密的方法:
import org.apache.tomcat.util.codec.binary.Base64;
import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* 使用Cipher类实现RSA加密解密
**/
public class RSAUtil {
/**
* 私钥
*/
private static final String privateKey = "";
/**
* 公钥
*/
private static final String publicKey = "";
/**
* 编码字符集
*/
public static final String CHARSET = "UTF-8";
/**
* 算法定义
*/
public static final String RSA_ALGORITHM = "RSA";
/**
* RSA公钥加密
*
* @param str 加密字符串
* @return 返回加密字符串的base64值
*/
public static String encrypt(String str) throws Exception {
//base64编码的公钥
byte[] decoded = Base64.decodeBase64(publicKey);
RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance(RSA_ALGORITHM).generatePublic(new X509EncodedKeySpec(decoded));
//RSA加密并base64编码
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
return Base64.encodeBase64String(cipher.doFinal(str.getBytes(CHARSET)));
}
/**
* RSA私钥解密
*
* @param str 加密字符串
* @return 返回解密后的明文
* @throws Exception 解密过程中的异常信息
*/
public static String decrypt(String str) throws Exception {
//64位解码加密后的字符串
byte[] inputByte = Base64.decodeBase64(str.getBytes(CHARSET));
//base64编码的私钥
byte[] decoded = Base64.decodeBase64(privateKey);
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance(RSA_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(decoded));
//RSA解密
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, priKey);
return new String(cipher.doFinal(inputByte));
}
}
前端RSA加密:
// 安装jsencrypt
// npm i jsencrypt -S
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
//公钥
const publicKey = ''
//私钥
const privateKey = ''
// 加密
export function encrypt(txt) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey) // 设置公钥
return encryptor.encrypt(txt) // 对数据
}
// 解密(暂无使用)
export function decrypt(txt) {
const encryptor = new JSEncrypt()
encryptor.setPrivateKey(privateKey) // 设置私钥
return encryptor.decrypt(txt) // 对数据进行解密
}
4.2 对称加密AES
加密和解密的方法:
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Base64Utils;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
/**
* AES加密工具类
*/
@Slf4j
public class AESUtil {
/**
* 编码
*/
private static final String ENCODING = "UTF-8";
/**
* 算法定义
*/
private static final String AES_ALGORITHM = "AES";
/**
* 指定填充方式
*/
private static final String CIPHER_PADDING = "AES/ECB/PKCS5Padding";
/**
* 密码
*/
private static final String AES_KEY = "your-private-key-xx";
/**
* AES加密
*
* @param content 待加密内容
* @return 加密后内容的base64值
*/
public static String encrypt(String content) {
if (StringUtils.isBlank(content)) {
return content;
}
try {
//对密码进行编码
byte[] bytes = AES_KEY.getBytes(ENCODING);
//设置加密算法,生成秘钥
SecretKeySpec secretKeySpec = new SecretKeySpec(bytes, AES_ALGORITHM);
// 算法/模式/补码方式
Cipher cipher = Cipher.getInstance(CIPHER_PADDING);
//选择加密
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
//处理待加密内容生成字节数组
byte[] encrypted = cipher.doFinal(content.getBytes(ENCODING));
//返回base64字符串
return Base64Utils.encodeToString(encrypted);
} catch (Exception e) {
log.error("AESUtil.encrypt content:{}, 加密异常", content, e);
return content;
}
}
/**
* 解密
*
* @param content 待解密内容
* @return 解密后的明文
*/
public static String decrypt(String content) {
if (StringUtils.isBlank(content)) {
return content;
}
try {
//对密码进行编码
byte[] bytes = AES_KEY.getBytes(ENCODING);
//设置解密算法,生成秘钥
SecretKeySpec secretKeySpec = new SecretKeySpec(bytes, AES_ALGORITHM);
// 算法/模式/补码方式
Cipher cipher = Cipher.getInstance(CIPHER_PADDING);
//选择解密
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
//先进行Base64解码
byte[] decodeBase64 = Base64Utils.decodeFromString(content);
//根据待解密内容进行解密
byte[] decrypted = cipher.doFinal(decodeBase64);
//将字节数组转成字符串
return new String(decrypted, ENCODING);
} catch (Exception e) {
log.error("AESUtil.decrypt content:{}, 解密异常", content, e);
return content;
}
}
}
注意,不管是RSA的公钥私钥,还是AES的密钥,都重新生成了一次,以防止用户选择相对简单的密码,而被攻击者破解或推断密钥
4.3 Nacos加解密的实现:Jasypt
Jasypt 其实是一个专门用于加解密的库,用的是对称加密AES。jasypt-spring-boot-starter用在SpringBoot项目中的步骤:
- 引入依赖
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
- 增加密钥配置
jasypt:
encryptor:
password: hello!!!
# 默认的加密算法是 PBEWITHHMACSHA512ANDAES_256,JDK9才支持,JDK1.8用不了,改为下面这个
algorithm: PBEWithMD5AndDES
这个password就别放nacos了,否则password泄漏,其余密文照样不安全,可放在项目jar包里的配置文件,或者直接不写在配置文件,只是让运维在java -jar是指定一下这个password值
- 引入Jasypt的加密类,改造Nacos中的明文
@Autowired
private StringEncryptor encryptor;
//明文变带有jasypt能识别前缀的密文
public String encrypt(String str) {
return "ENC(" + encryptor.encrypt(str) + ")";
}
// 生成结果如:ENC(GT2vTn1+SdeFu90xH/vgw3uYTNyV5PGp),替换Nacos中对应的明文
- 前面提到,Jasypt自己会识别是否为自己的密文,然后解密后重新赋值,所以改造后取值,依旧像之前一样直接取即可
@Value("${spring.redis.password}")
private String password;
5、历史数据兼容处理
对旧的明文存储的数据,需要处理为密文,系统有定时任务管理页面的话,可考虑加个定时任务,给运维人员去执行一次。如果没有,可考虑提供一个内部接口,调用一次,处理旧数据。