前言
日常使用的加密的时机,包括后端存储密码信息,API接口的认证鉴权,前后端敏感数据加密传输等等,为了保证信息的不被泄漏,也为了验证信息的准确性
base64编解码
用于将二进制数据转换为可打印的 ASCII 字符串
作用场景: 用于请求数据传输中,文件嵌入HTML/CSS,加密密钥传输等
第一种:当我们使用hutool
编码实现,支持对inputStream,字符数组,文件等加密
less
// 编码
Base64.encode("123".getBytes());
// 解码
Base64.decodeStr(encode);
第二种: 当我们使用java
原生编码实现
ini
// 编码为 Base64
String encodedString = Base64.getEncoder().encodeToString("123".getBytes());
// 解码为原始字符串
byte[] decodedBytes = Base64.getDecoder().decode(encodedString);
String decodedString = new String(decodedBytes);
注意: Base64只是编码方式,不是加密方式,不能代替加密算法,而且编码会增加33%的数据大小
对称加密
双方使用同一个密钥来进行加解密,加密速度快,但是问题就是密钥泄漏容易导致数据不安全。
目前市面上常用的加密算法:
AES
比较推荐
AES实现
- AES的工作模式包含:
ECB
(不太安全),CBC
(推荐,需要提供IV),GCM
(更安全,不需要填充方式) - AES的填充方式: 常用的有
PKCS5Padding
或PKCS7Padding
- AES的密钥长度可以为128位(16字节),192位(24字节),256位(32字节)
- AES的IV必须是16字节
第一种 :当我们使用hutool
加密实现, 其中下文中的content
为待加密的内容, 注意:hutool
将对称加密都封装在symmetric
包中 ,默认使用ECB
模式和 PKCS5Padding
填充方式
ini
// 生成密钥,AES 默认密钥长度为 128位(keySize可以指定位数)
byte[] keyBytes = SecureUtil.generateKey(SymmetricAlgorithm.AES.getValue()).getEncoded();
// 创建 AES 加密对象,使用 ECB 模式和 PKCS5Padding 填充方式
AES aes = SecureUtil.aes(keyBytes);
// 加密十六进制字符串结果
String encrypt = aes.encryptHex(content);
// 加密base64结果
String encrypt = aes.encryptBase64(content);
// 解密
String decryptStr = aes.decryptStr(encrypt);
如果你想使用密钥便于传输或者存储,可以转换成base64
结果/Hex
十六进制字符串。
scss
// 将字节数组编码为 Base64 字符串
Base64.getEncoder().encodeToString(keyBytes);
// 十六进制字符串,可以使用 Hutool 的 HexUtil
String hexKey = HexUtil.encodeHexStr(encodedKey);
如果你需要修改其他加密模式CBC
/填充模式NoPadding
,需要手动构建AES对象, 注意使用CBC
需要初始化响亮(IV)
ini
private static final String INIT_VECTOR = "1234567890123456";
AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, keyBytes, INIT_VECTOR.getBytes());
第二种:当我用java原生来实现
KeyGenerator
来实现生成AES密钥, 也可以通过自己有了密钥通过使用 SecretKeySpec
将字节数组转换为密钥对象,
ini
private static final String INIT_VECTOR = "1234567890123456";
// 生成AES密钥
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(128); // 设置密钥长度
SecretKey secretKey = keyGen.generateKey()
// 通过自己的密钥给定
byte[] keyBytes = "my-secret-key-123".getBytes(); // 必须是 16、24 或 32 字节
SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");
使用 Cipher
类初始化加密模式 , 然后配置工作模式和填充方式,最后加密以base64
结果
ini
// 加密
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivSpec = new IvParameterSpec(INIT_VECTOR.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
byte[] encryptedBytes = cipher.doFinal(plainText.getBytes());
String result = Base64.getEncoder().encodeToString(encryptedBytes);
// 解密
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivSpec = new IvParameterSpec(INIT_VECTOR.getBytes());
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
byte[] decodedBytes = Base64.getDecoder().decode(encryptedText);
byte[] decryptedBytes = cipher.doFinal(decodedBytes);
String result = new String(decryptedBytes);
非对称加密
使用一对密钥(公钥和私钥),公钥加密,私钥解密。安全性能高,但速度可能会慢。
目前市面上常用的加密算法:
RSA
比较推荐
RSA 实现
现在主要应用的格式为PEM格式,公钥文件扩展名.pem
, .pub
. 私钥文件扩展名.pem
, .key
,以-----BEGIN RSA PRIVATE KEY-----
或-----BEGIN PUBLIC KEY-----
开头,以-----END RSA PRIVATE KEY-----
或-----END PUBLIC KEY-----
结尾,中间是经过 Base64 编码的密钥数据
第一种 :当我们使用hutool
加密实现
ini
// 自动生成密钥对 , 对应base64结果的公钥和私钥
RSA rsa = new RSA();
// 如果是有公钥和私钥,可以自定义初始化
// RSA rsa = new RSA(privateKey, publicKey);
String publicKey = rsa.getPublicKeyBase64();
String privateKey = rsa.getPrivateKeyBase64();
通过调用encryptBase64
来实现加密,decryptStr
来实现解密
ini
// 使用公钥加密
String encrypted = rsa.encryptBase64(plainText, KeyType.PublicKey);
// 使用私钥解密
String decrypted = rsa.decryptStr(encrypted, KeyType.PrivateKey);
如果有对于数据签名进行验签需求,可以使用sha256 with rsa签名,可以通过sign来实现
ini
byte[] data = "12345".getBytes();
Sign sign = SecureUtil.sign(SignAlgorithm.MD5withRSA);
// Sign sign = SecureUtil.sign(SignAlgorithm.MD5withRSA,privateKeyBase64, publicKeyBase64);
//签名
byte[] signed = sign.sign(data);
//验证签名
boolean verify = sign.verify(data, signed);
第二种 :当我用java原生来实现,公钥通常以 X.509 标准格式进行编码,所以使用X509EncodedKeySpec
ini
// 获取公钥 , 字符串publicKeyContent为公钥base64加密内容,剔除 BEGIN和END
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyContent);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
// 获取私钥,字符串privateKeyContent为私钥base64加密内容,剔除 BEGIN和END
byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyContent);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
使用 Cipher
类初始化加密模式使用公钥/私钥,来进行加解密
ini
// 加密,最后以base64结果输出
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedBytes = cipher.doFinal(plainText.getBytes());
String encode = Base64.getEncoder().encodeToString(encryptedBytes);
// 解密,先将获取到的base64结果转换,最后还原字符串原文
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
String s = new String(decryptedBytes);
注意: 公钥可以对所有人公开获取,私钥是私密的只有密钥持有者才有。 使用公钥加密->只有私钥能解密 (对应场景:加密数据) 使用私钥加密->所有公钥都能解开 (对应场景:验签)
所以一般使用场景就是对数据加密,使用前端公钥加密,后端私钥才能解开。对于服务器发送的数据带有私钥签名,所有的公钥都能验证签名。
哈希算法
通过单向的加密方式,生成固定长度的摘要值,注意无法逆向还原数据,一般用于比对数据完整性校验。
目前市面上常用的加密算法:
MD5
,SHA系列
,HMAC
,bcrypt
MD5 实现
MD5
: 主要应用场景,还是针对验证数据完整性,防止内容被篡改(但容易收到碰撞攻击)
第一种使用原生java实现,
ini
// 创建 MD5 摘要实例
MessageDigest md = MessageDigest.getInstance("MD5");
// 计算哈希值
md.update("12345".getBytes());
byte[] digest = md.digest();
// 后续在通过字节数组转换为十六进制字符串
第二种使用hutool
实现
ini
String s = SecureUtil.md5("12345");
// 也可自己定义构建实现
MD5 md5 = new MD5();
String s1 = md5.digestHex("12345");
SHA实现
SHA
:主要场景: 密码存储,数字签名,验证数据完整性等。
目前SHA-1
容易收到碰撞攻击,安全性不高。SHA-2
目前是安全的,SHA-3
是作为是作为前者SHA-2
的备用
对于SHA-2的变种,不同的输出的长度
- SHA-256 :输出 256 位(64 个十六进制字符)。
- SHA-384 :输出 384 位(96 个十六进制字符)。
- SHA-512 :输出 512 位(128 个十六进制字符)。
第一种使用java原生的实现 针对SHA-256
ini
MessageDigest md = MessageDigest.getInstance("SHA-256");
// 计算哈希值
md.update("12345".getBytes());
byte[] digest = md.digest();
// 后续在通过字节数组转换为十六进制字符串
第二种使用hutool
的实现
ini
String sha256Hex = DigestUtil.sha256Hex("12345");
在密码存储等场景中,通常需要对输入数据进行加盐(Salt)处理以增加安全性, 可以将输入值和Salt字符串拼接来实现加密。
HMAC系列实现
HMAC系列
: 主要应用场景是验签等操作,需要密钥
常见的HMAC
算法 :
HMAC-MD5
HMAC-SHA1
HMAC-SHA256
HMAC-SHA512
通过密钥配合来实现HMAC的加解密
第一种: 通过java原生实现
ini
// 创建 Mac 实例
Mac mac = Mac.getInstance("HmacSHA256");
// 将密钥转换为 SecretKeySpec
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), algorithm);
// 初始化 Mac 对象
mac.init(secretKeySpec);
// 更新输入数据
mac.update(message.getBytes());
// 计算 HMAC 值
byte[] hmacBytes = mac.doFinal();
第二种:使用Hutool来实现
ini
SecretKey secretKey = SecureUtil.generateKey(HmacAlgorithm.HmacSHA256.getValue())
Hmac hmac = new Hmac(HmacAlgorithm.HmacSHA256,secretKey)
hmac.digest("12345")
bcrypt实现
bcrypt
: 主要应用场景密码存储和身份验证,由于计算速度慢,不适合存储哈希大量数据。
第一种:使用Spring Security
提供的 BCryptPasswordEncoder
类
xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.8.0</version> <!-- 请根据需要选择最新版本 -->
</dependency>
注意: BCryptPasswordEncoder
内部已经实现了加盐机制,无需手动设置盐值。每次调用 encode
方法时,它会自动生成一个随机盐值,并将其嵌入到最终的哈希结果中
ini
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 生成加密后的密码
String hashedPassword = encoder.encode("mySecurePassword123");
// 验证密码
boolean isMatch = encoder.matches(rawPassword, hashedPassword);
第二种通过hutool
实现
arduino
// 加密
String result = DigestUtil.bcrypt("12345")
// 匹配
boolean flag = DigestUtil.bcryptCheck("12345","hashed")
混合加密
混合加密结合了对称加密和非对称加密的优点,既保证了安全性,又提高了性能。
两者各自起到的作用为:
- 通过非对称加密(如RSA)传递会话密钥
- 通过对称加密(如AES)使用会话密钥来加解密传输数据
例如我们现在日常使用的HTTPS的加解密过程,具体流程步骤如下:
- 客户端发起Https请求
- 服务器返回对应证书(公钥和CA签名)
- 客户端验证证书(有效期,是否受信任CA签发,证书和访问域名是否一致)
- 密钥协商(对应我们上述所说的混合加密)
- 生成随机数,使用证书公钥加密
- 服务器用私钥解密,得到随机数
- 客户端和服务端根据这个随机数和其他握手信息生成相同会话密钥
- 后续通过这个密钥来加密传输数据
- 直到会话结束,会话密钥被丢弃
综合应用场景
二进制内容的传输和展示
对于应用到二进制数据的传输,文件图片等信息
- 针对图片内容,前端传输字符串的base64的编码数据
- 前端传输的base64内容
- 对于一些密钥信息存在二进制数组内容,可以通过直接将其转换为base64的文本形式
- url的上的编码问题,可以通过base64来实现
ini
// 后端获取到byte[]二进制内容 ,获取到流能后续操作
byte[] content = Base64.decodeStr(encode);
InputStream inputStream = new ByteArrayInputStream(imageBytes);
// 对于加密的数据可以通过base64的形式来传输
byte[] encryptedBytes = cipher.doFinal(plainText.getBytes());
String base64Content = Base64.getDecoder().decode(byteArray)
前后端传输私密数据
例如在用户登录,支付信息,个人隐私数据等接口的调用上,可以增加混合加密。
- 首先后端通过非对称加密(RSA)生成一对公钥和私钥。公钥由前端存储,私钥由后端存储。
- 前端请求接口,通过算法生成AES的会话密钥
- 前端使用RSA加密会话密钥
- 前端使用AES的密钥来加密传输数据
- 前端将加密的会话密钥和加密的传输数据发送给后端
- 后端通过私钥来解开加密的会话密钥,获得会话密钥,然后通过会话密钥来解开加密数据
验证身份
可以通过sha256 with ras
来实现对数据签名, 然后通过对签名数据来验签,判定是否是可靠的用户传递的,以此来确定身份。
注意:传递的签名,是由私钥签名获得,验签名可以通过公钥。
ini
Sign sign = SecureUtil.sign(SignAlgorithm.MD5withRSA,privateKeyBase64, publicKeyBase64);
//签名
byte[] signed = sign.sign(data);
//验证签名 ,通过数据和签名来验证
boolean verify = sign.verify(data, signed);
也可以使用 hmac-sha256
ini
Hmac hmac = SecureUtil.hmacSha256(key)
byte[] digest = hmac.digest(data)
// 比较两个签名数组
hmac.verify(digest,digestToCompare)
密码的加密
对于注册用户的时候,对于密码的加密保存在后端,可以使用bcrypt
的方式来加密进行存储。后续崽用户登录的时候,可以将这个登录的密码加密和数据库存储的密码比对是否一致即可。
ini
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 生成加密后的密码
String hashedPassword = encoder.encode("mySecurePassword123");
// 验证密码
boolean isMatch = encoder.matches(rawPassword, hashedPassword);
标识文件哈希值
对于不同文件,我们在上传文件的时候不可能比对整个文件是否一致,而是通过比对哈希值是否一致性,就能判定是否两个文件相同。计算文件的哈希值需要参考的内容需要思考一下,
- 可以通过文件头部,中部和尾部的组合
- 可以随机性的都固定选取指定部分内容
- 结合一些文件大小等信息综合判定
scss
// 读取文件的头部、中间和尾部
byte[] head = readBytes(file, 0, 1024); // 头部 1KB
byte[] middle = readBytes(file, fileSize / 2, 1024); // 中间 1KB
byte[] tail = readBytes(file, fileSize - 1024, 1024); // 尾部 1KB
// 将数组拼接
byte[] combined = concatenate(head, middle, tail);
// 最后计算哈希值
return calculateHash(combined, "SHA-256");
到底是使用md5还是sha呢?如果是对于想快速,安全性可以降低点的选择md5,如果对于安全性高的选择sha
参考第三方使用案例
微信支付
对于微信支付的验签名场景:
- 微信支付应答商户的请求时,商户需要验签(验证请求是来自微信支付)
- 接收微信支付的回调时,商户需要验签(验证请求是来自微信支付)
主要是通过:sha256 with rsa
来实现验签名,对于响应数据的结果拼接和签名比对是否签名正确
支付宝
- 对于支付宝api请求,需要用户传递支付宝对参数的签名,赋值在请求头上
主要通过对参数的排序,然后通过RSA
来实现加密
- 对于支付宝api请求的处理结果,通过支付宝服务器的私钥进行加密
主要通过RSA
来实现加密,用户通过支付宝公钥来实现验签名
shopee 和 tiktok
对于用户的API请求,使用对参数进行hmacsha256
的加密