文章目录
- 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 -Simport 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、历史数据兼容处理
对旧的明文存储的数据,需要处理为密文,系统有定时任务管理页面的话,可考虑加个定时任务,给运维人员去执行一次。如果没有,可考虑提供一个内部接口,调用一次,处理旧数据。