大家好,我是星仔。本博客收录于《华星详谈-学习中心》。本学习中心收集了Java整个技术体系的所有技术要点。每篇博客后面或者知识点结尾都附带有面试题,提供给大家巩固本章内容。
为各位同胞们能够系统性的掌握整个Java技术体系而建立的学习中心。星仔正在努力的更新学习中心中的内容。望诸君共勉!!!
在文章的开头,博主主要来说明一下为啥博主会在加解密的基础上实现以下功能这些功能。
答:在信息加密的基础上,进行信息的防篡改主要是为了防止非法用户破解请求信息后,对信息重新进行修改之后在传输过来。一般我们是对一个数据(data)进行非对称加密之后,再根据这个数据(data)的内容进行数字签名,也就是使用摘要算法对原数据(data)再次进行加密后,把非对称加密后的数据以及数字签名数据都传递给后台。具体的实现方式如下图所示:
答:其实在很多的业务场景中对于数据的时效性以及使用次数都有要求。基于这些使用场景,博主特意在加解密的基础上实现了一码一检、过期失效的功能。
博主在本文中的所有功能实现了两种方式,分别是基于内存以及基于Redis的方式实现一码一检、过期失效的功能。最主要的原因是兼容有些项目中并没有引用Redis,这时基于内存的实现方式是对其他三方功能依赖最小的。
在项目中,无论是和其他系统进行交互还是安全测评那一关,数据的加解密功能都是必不可少的。在本博客中星仔主要以Hutool开源工具类库为依托,来说明以及实现加解密中关于信息防篡改、一码一检、过期失效、多实现方式等功能。
主要实现的功能点有:
Hutool工具包官方文档:https://hutool.cn/docs/#/crypto/%E5%8A%A0%E5%AF%86%E8%A7%A3%E5%AF%86%E5%B7%A5%E5%85%B7-SecureUtil
加密分为三种:
- 对称加密(symmetric),例如:AES、DES等
- 非对称加密(asymmetric),例如:RSA、DSA等
- 摘要加密(digest),例如:MD5、SHA-1、SHA-256、HMAC等
hutool针对这三种加密类型分别封装,并提供常用的大部分加密算法。
对于非对称加密,实现了:
对于对称加密,实现了:
对于摘要算法实现了:
其中,针对常用到的算法,模块还提供SecureUtil工具类用于快速实现加密。
在hutool工具类库的功能上,星仔做了关于一码一检、过期失效、信息防篡改、多实现方式的功能,主要涉及到的类有以下四个类:
具体的代码如下所示:
Rsa加解密抽象类,主要定义了一些公共实现的内容;
/**
* @description:Rsa加解密
* @author: yaogx
* @time: 2022/4/8 17:17
*/
public abstract class AbstractRsa {
/**
* 常量,表示一个key永不过期 (在一个key被标注为永远不过期时返回此值)
*/
public final long NEVER_EXPIRE = -1;
/**
* 常量,表示系统中不存在这个缓存 (在对不存在的key获取剩余存活时间时返回此值)
*/
public final long NOT_VALUE_EXPIRE = -2;
/**
* 过期时间,默认是1小时
*/
public final long TIME = 1 * 60 * 60;
/**
* 私钥
*/
String privateKey = "";
/**
* 公钥
*/
String publicKey = "";
/**
* 初始化
*/
void initRefreshThread() {
}
/**
* RSA加密
*
* @param data 需要加密的数据
* @param digest 摘要密文,使用MD5(计算32位MD5摘要值,并转为16进制字符串)对data数据进行加密 如:DigestUtil.md5Hex(data)
* @return
*/
public String encrypt(String data, String digest) {
if (StringUtils.isAllBlank(data, digest)) {
throw new ApiException(ApiErrorCodeEnum.PARAMETER_CALIBRATION);
}
//校验摘要密文,防止信息篡改
String md5Hex = DigestUtil.md5Hex(data);
if (!md5Hex.equals(digest)) {
throw new ApiException(ApiErrorCodeEnum.DIGEST_CALIBRATION);
}
RSA rsa = SecureUtil.rsa(null, publicKey);
return rsa.encryptBase64(data, KeyType.PublicKey);
}
/**
* RSA加密(取消了数字签名,不推荐使用)
*
* @param data 需要加密的数据
* @return
*/
public String encrypt(String data) {
if (StringUtils.isAllBlank(data)) {
throw new ApiException(ApiErrorCodeEnum.PARAMETER_CALIBRATION);
}
RSA rsa = SecureUtil.rsa(null, publicKey);
return rsa.encryptBase64(data, KeyType.PublicKey);
}
/**
* RSA解密(取消了数字签名验证,不推荐使用)
*
* @param data 需要解密的数据
* @return
*/
public String decrypt(String data) {
RSA rsa = SecureUtil.rsa(privateKey, null);
return rsa.decryptStr(data, KeyType.PrivateKey);
}
/**
* RSA解密
*
* @param data 需要解密的密文数据
* @param digest 摘要密文,使用MD5(计算32位MD5摘要值,并转为16进制字符串)对data数据进行加密 如:DigestUtil.md5Hex(data)
* @return
*/
public String decrypt(String data, String digest) {
//校验摘要密文,防止信息篡改
String md5Hex = DigestUtil.md5Hex(data);
if (!md5Hex.equals(digest)) {
throw new ApiException(ApiErrorCodeEnum.DIGEST_CALIBRATION);
}
RSA rsa = SecureUtil.rsa(privateKey, null);
return rsa.decryptStr(data, KeyType.PrivateKey);
}
}
RSA加解密具体操作工具类(工厂类)
/**
* @description:RSA加解密具体操作工具类(工厂类)
* @author: yaogx
* @time: 2022/4/2 10:48
*/
@Slf4j
public class RsaUtils {
/**
* 获取对应的RSA实现类对象
*
* @param clazz
* @return
*/
public static AbstractRsa getRSA(Class<? extends AbstractRsa> clazz) {
if (null != clazz) {
try {
AbstractRsa iRsaDao = clazz.newInstance();
//启用定时器操作
iRsaDao.initRefreshThread();
return iRsaDao;
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage(), e);
}
}
return null;
}
/**
* 获取默认的RSA实现类对象
*
* @return
*/
public static AbstractRsa getRSA() {
try {
AbstractRsa iRsaDao = RsaDefaultImpl.class.newInstance();
//启用定时器操作
iRsaDao.initRefreshThread();
return iRsaDao;
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage(), e);
}
return null;
}
}
RSA加解密存储方式,默认存储到内存中
/**
* @description:RSA加密存储方式,默认存储到内存中
* @author: yaogx
* @time: 2022/4/8 17:04
*/
public class RsaDefaultImpl extends AbstractRsa {
/**
* 数据集合
*/
public static Map<String, Object> dataMap = new ConcurrentHashMap<String, Object>();
/**
* 过期时间集合 (单位: 毫秒) , 记录所有key的到期时间 [注意不是剩余存活时间]
*/
public static Map<String, Long> expireMap = new ConcurrentHashMap<String, Long>();
/**
* 默认dao层实现类中,每次清理过期数据间隔的时间 (单位: 秒) ,默认值30秒,设置为-1代表不启动定时清理
*/
private int dataRefreshPeriod = 30;
/**
* 执行数据清理的线程
*/
public static Thread refreshThread;
/**
* 是否继续执行数据清理的线程标记
*/
public volatile boolean refreshFlag;
/**
* RSA加密
*
* @param data 需要加密的数据
* @param digest 摘要密文,使用MD5(计算32位MD5摘要值,并转为16进制字符串)对data数据进行加密 如:DigestUtil.md5Hex(data)
* @return
*/
@Override
public String encrypt(String data, String digest) {
String encrypt = super.encrypt(data, digest);
//放入内存中
set(data, encrypt, TIME);
return encrypt;
}
@Override
public String encrypt(String data) {
String encrypt = super.encrypt(data);
//放入内存中
set(data, encrypt, TIME);
return encrypt;
}
/**
* RSA解密
*
* @param data 需要解密的密文数据
* @param digest 摘要密文,使用MD5(计算32位MD5摘要值,并转为16进制字符串)对data数据进行加密 如:DigestUtil.md5Hex(data)
* @return
*/
@Override
public String decrypt(String data, String digest) {
String decrypt = super.decrypt(data, digest);
//校验在内存中是否存在
Object o = dataMap.get(decrypt);
if (o == null) {
throw new ApiException(ApiErrorCodeEnum.RSA_CHECK_DATA);
}
//解密之后从内存中删除
delete(decrypt);
return decrypt;
}
/**
* RSA解密(取消了数字签名验证,不推荐使用)
*
* @param data 需要解密的数据
* @return
*/
@Override
public String decrypt(String data) {
String decrypt = super.decrypt(data);
//校验在内存中是否存在
Object o = dataMap.get(decrypt);
if (o == null) {
throw new ApiException(ApiErrorCodeEnum.RSA_CHECK_DATA);
}
//解密之后从内存中删除
delete(decrypt);
return decrypt;
}
/**
* 写入Value,并设定存活时间 (单位: 秒)
*
* @param key 键名称
* @param value 值
* @param timeout 过期时间(值大于0时限时存储,值=-1时永久存储,值=0或小于-2时不存储)
*/
private void set(String key, String value, long timeout) {
if (timeout == 0 || timeout <= NOT_VALUE_EXPIRE) {
return;
}
dataMap.put(key, value);
expireMap.put(key, (timeout == NEVER_EXPIRE) ? (NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));
}
/**
* 删除Value
*
* @param key 键名称
*/
public void delete(String key) {
dataMap.remove(key);
expireMap.remove(key);
}
@Override
public void initRefreshThread() {
// 启动定时刷新
this.refreshFlag = true;
this.refreshThread = new Thread(() -> {
for (; ; ) {
try {
try {
// 如果已经被标记为结束
if (refreshFlag == false) {
return;
}
// 执行清理
refreshDataMap();
} catch (Exception e) {
e.printStackTrace();
}
// 休眠N秒
int dataRefreshPeriod = getDataRefreshPeriod();
if (dataRefreshPeriod <= 0) {
dataRefreshPeriod = 1;
}
Thread.sleep(dataRefreshPeriod * 1000);
} catch (Exception e) {
e.printStackTrace();
}
}
});
this.refreshThread.start();
}
/**
* 清理所有已经过期的key
*/
public void refreshDataMap() {
Set<String> keySet = expireMap.keySet();
if (CollectionUtils.isNotEmpty(keySet)) {
Iterator<String> keys = keySet.iterator();
while (keys.hasNext()) {
clearKeyByTimeout(keys.next());
}
}
}
/**
* 如果指定key已经过期,则立即清除它
*
* @param key 指定key
*/
void clearKeyByTimeout(String key) {
Long expirationTime = expireMap.get(key);
// 清除条件:如果不为空 && 不是[永不过期] && 已经超过过期时间
if (expirationTime != null && expirationTime != NEVER_EXPIRE && expirationTime < System.currentTimeMillis()) {
dataMap.remove(key);
expireMap.remove(key);
}
}
/**
* @return 默认dao层实现类中,每次清理过期数据间隔的时间 (单位: 秒) ,默认值30秒,设置为-1代表不启动定时清理
*/
public int getDataRefreshPeriod() {
return dataRefreshPeriod;
}
/**
* 结束定时任务
*/
public void endRefreshThread() {
this.refreshFlag = false;
}
}
RSA加解密Redis的存储方式,一般放在具体的使用项目中,因为很多微服务项目基础依赖包(common)中并没有引入Redis,若是有引入的话直接放在一起也是可以的。
/**
* @description:RSA加密存储方式之存储到Redis中
* @author: yaogx
* @time: 2022/4/8 19:10
*/
@Service
public class RsaRedisImpl extends AbstractRsa {
/**
* String专用
*/
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public String encrypt(String data, String digest) {
String encrypt = super.encrypt(data, digest);
//放入redis中,并设置过期时间限制
set(data, encrypt, TIME);
return encrypt;
}
@Override
public String encrypt(String data) {
String encrypt = super.encrypt(data);
//放入redis中,并设置过期时间限制
set(data, encrypt, TIME);
return encrypt;
}
@Override
public String decrypt(String data) {
String decrypt = super.decrypt(data);
//校验在Redis中是否存在,若存在则删除
checkAndDelete(decrypt);
return decrypt;
}
private void checkAndDelete(String decrypt) {
//校验在Redis中是否存在
String key = stringRedisTemplate.opsForValue().get(decrypt);
if (StringUtils.isBlank(key)) {
throw new ApiException(ApiErrorCodeEnum.RSA_CHECK_DATA);
}
//删除指定key
stringRedisTemplate.delete(key);
}
@Override
public String decrypt(String data, String digest) {
String decrypt = super.decrypt(data, digest);
//校验在Redis中是否存在,若存在则删除
checkAndDelete(decrypt);
return decrypt;
}
/**
* 写入Value,并设定存活时间 (单位: 秒)
*
* @param key 键名称
* @param value 值
* @param timeout 过期时间(值大于0时限时存储,值=-1时永久存储,值=0或小于-2时不存储)
*/
private void set(String key, String value, long timeout) {
if (timeout == 0 || timeout <= NOT_VALUE_EXPIRE) {
return;
}
if (timeout == NEVER_EXPIRE) {
stringRedisTemplate.opsForValue().setIfAbsent(key, value);
} else {
//TimeUnit.MINUTES单位为60秒,故此处需要除掉
timeout = TIME / 60;
stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.MINUTES);
}
}
}
以下例子中,digest参数应该交由前端传过来。下面我是为了方便直接在后端生成了数字签名。
/**
* @description:加解密
* @author: yaogx
* @time: 2022/4/6 9:36
*/
@RestController
@RequestMapping("/encryptionAndDecryption")
@Api(value = "EncryptionAndDecryptionController", tags = {"加解密管理"})
public class EncryptionAndDecryptionController {
@ApiOperation("RSA加密(带数字签名)")
@PostMapping("/rsa/encryptionAndDigest")
public Result<String> encryptionAndDigest(String data) {
return Result.success(RsaUtils.getRSA().encrypt(data, DigestUtil.md5Hex(data)));
}
@ApiOperation("RSA解密(带数字签名)")
@PostMapping("/rsa/decryptionAndDigest")
public Result<String> rsaDecryptionAndDigest(String data) {
return Result.success(RsaUtils.getRSA().decrypt(data, DigestUtil.md5Hex(data)));
}
@ApiOperation(" RSA加密(不带数字签名,不推荐使用)")
@PostMapping("/rsa/encryption")
public Result<String> encryption(String data) {
return Result.success(RsaUtils.getRSA().encrypt(data));
}
@ApiOperation("RSA解密(不带数字签名,不推荐使用)")
@PostMapping("/rsa/decryption")
public Result<String> rsaDecryption(String data) {
return Result.success(RsaUtils.getRSA().decrypt(data));
}
@ApiOperation("RSA加密(带数字签名)- Redis版本")
@PostMapping("/rsa/encryptionAndDigestByRedis")
public Result<String> encryptionAndDigestByRedis(String data) {
return Result.success(RsaUtils.getRSA(RsaRedisImpl.class).encrypt(data, DigestUtil.md5Hex(data)));
}
@ApiOperation("RSA解密(带数字签名)- Redis版本")
@PostMapping("/rsa/decryptionAndDigestByRedis")
public Result<String> rsaDecryptionAndDigestByRedis(String data) {
return Result.success(RsaUtils.getRSA(RsaRedisImpl.class).decrypt(data, DigestUtil.md5Hex(data)));
}
@ApiOperation(" RSA加密(不带数字签名,不推荐使用)- Redis版本")
@PostMapping("/rsa/encryptionByRedis")
public Result<String> encryptionByRedis(String data) {
return Result.success(RsaUtils.getRSA(RsaRedisImpl.class).encrypt(data));
}
@ApiOperation("RSA解密(不带数字签名,不推荐使用)- Redis版本")
@PostMapping("/rsa/decryptionByRedis")
public Result<String> rsaDecryptionByRedis(String data) {
return Result.success(RsaUtils.getRSA(RsaRedisImpl.class).decrypt(data));
}
}
整个关于实现加解密技术中的信息防篡改、一码一检、过期失效、多种实现方式文章就到这里结束了,若是各位看官觉得有用麻烦给个三连。