目录
效果
RSA介绍
实现思路
服务端实现
RSAService:RSA算法的相关操作
RedisService:公钥和密钥的存储和获取
获取公钥的接口
客户端使用公钥加密
服务端使用私钥解密
效果
RSA介绍
RSA是一种非对称加密算法。
非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。 非对称加密算法实现机密信息交换的基本过程是:甲方生成一对密钥并将公钥公开,需要向甲方发送信息的其他角色(乙方)使用该密钥(甲方的公钥)对机密信息进行加密后再发送给甲方;甲方再用自己私钥对加密后的信息进行解密。甲方想要回复乙方时正好相反,使用乙方的公钥对数据进行加密,同理,乙方使用自己的私钥来进行解密。
在我们Web环境中,甲方即为我们的服务端,乙方即为客户端。服务端需要生成一个公钥和一个私钥,私钥保管在服务端不能泄露,公钥可以公开给所有客户端。
实现思路
- 服务端在启动时,生成一个公钥和一个私钥。由于是分布式环境,需要将公钥和私钥存储于Redis,并设置过期时间。公钥和私钥的被动 “续期”,参见代码实现。
- 为了公开向客户端公开公钥,服务端需要提供一个获取公钥的接口,该接口从Redis中获取公钥返回给客户端。
- 登录页面(客户端)通过公钥将登录密码加密,通过http请求服务端
- 服务端拿到加密后的密码(密文),从Redis中获取私钥解密,得到原始密码。将原始密码以注册时相同的加密规则加密,与数据库中存储的密码比对,即可判断密码是否正确
服务端实现
下面的所有实现代码均在:https://github.com/passerbyYSQ/forum
RSAService:RSA算法的相关操作
/**
* @author passerbyYSQ
* @create 2022-09-25 16:42
*/
public interface RSAService {
KeyPair generateKeyPair();
String getPublicKey();
String getPrivateKey();
String decryptByPrivateKey(String encryptedText);
String encryptByPublicKey(String rawText);
}
/**
* @author passerbyYSQ
* @create 2022-09-25 15:36
*/
@Service
@Slf4j
public class RSAServiceImpl implements RSAService, ApplicationListener<ContextRefreshedEvent> {
@Resource
private RedisService redisService;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 对于web应用会出现父子容器,这样就会触发两次
if (event.getApplicationContext().getParent() == null) {
redisService.saveRSAKeyPair();
log.info("成功将RSA的KeyPair缓存至Redis");
}
}
@Override
public KeyPair generateKeyPair() {
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(1024, new SecureRandom());
return generator.generateKeyPair();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
public String getPublicKey() {
KeyPair keyPair = redisService.getRSAKeyPair();
byte[] publicBytes = keyPair.getPublic().getEncoded();
byte[] base64Bytes = Base64.getEncoder().encode(publicBytes);
return new String(base64Bytes);
}
public String getPrivateKey() {
KeyPair keyPair = redisService.getRSAKeyPair();
byte[] privateBytes = keyPair.getPrivate().getEncoded();
byte[] base64Bytes = Base64.getEncoder().encode(privateBytes);
return new String(base64Bytes);
}
public String decryptByPrivateKey(String encryptedText) {
if (ObjectUtils.isEmpty(encryptedText)) {
return null;
}
try {
KeyPair keyPair = redisService.getRSAKeyPair();
byte[] decodedBytes = Base64.getDecoder().decode(encryptedText.getBytes());
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
byte[] decryptedBytes = cipher.doFinal(decodedBytes);
return new String(decryptedBytes);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public String encryptByPublicKey(String rawText) {
if (ObjectUtils.isEmpty(rawText)) {
return null;
}
try {
KeyPair keyPair = redisService.getRSAKeyPair();
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
byte[] encryptedBytes = cipher.doFinal(rawText.getBytes());
byte[] base64Bytes = Base64.getEncoder().encode(encryptedBytes);
return new String(base64Bytes);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
RedisService:公钥和密钥的存储和获取
/**
* @author passerbyYSQ
* @create 2021-06-02 13:18
*/
public interface RedisService {
void saveRSAKeyPair();
KeyPair getRSAKeyPair();
}
/**
* @author passerbyYSQ
* @create 2021-06-02 13:21
*/
@Slf4j
@Service
public class RedisServiceImpl implements RedisService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RSAService rsaService;
@Override
public void saveRSAKeyPair() {
String key = String.format(Constant.REDIS_KEY_KEY_PAIR, "RSA");
KeyPair keyPair = rsaService.generateKeyPair();
if (!ObjectUtils.isEmpty(keyPair)) {
redisTemplate.opsForValue().setIfAbsent(key, keyPair, Constant.DURATION_KEY_PAIR);
}
}
@Override
public KeyPair getRSAKeyPair() {
String key = String.format(Constant.REDIS_KEY_KEY_PAIR, "RSA");
KeyPair keyPair = (KeyPair) redisTemplate.opsForValue().get(key);
if (ObjectUtils.isEmpty(keyPair)) {
saveRSAKeyPair(); // KeyPair过期,则重新续费
}
// 重新获取KeyPair,因为如果并发续费,成功续上的有可能不是本机生成的KeyPair
return (KeyPair) redisTemplate.opsForValue().get(key);
}
}
获取公钥的接口
Controller中新增一个方法,调用RedisService中的RsaService的getPublicKey()方法获取公钥。此处略,在项目中我是直接通过模板引擎,将公钥植入到登录页面的。如下图,代码仅供参考,思路是一样的。
客户端使用公钥加密
前端使用的加密类库是 jsencrypt(https://github.com/travist/jsencrypt),js文件下载:https://github.com/travist/jsencrypt/tree/master/bin
使用比较简单,参见上面截图
服务端使用私钥解密
服务端调用 RsaService的decryptByPrivateKey()方法解密即可获得明文。下面代码截图,仅供参考,思路是一样的。