【笑小枫系列】Java加密那点事,本文给你讲的明明白白

news2024/11/19 11:17:59

本文简介

相信大家在日常工作中都遇到过加密的场景吧,像登录密码加密保存、无token接口验签、数据加密传输等等。

本文将详细的介绍一下加密的方式,并分析使用场景,并会以详细的代码完整的介绍如何使用加密,让小伙伴们遇到加密时,有更好的选择。

img

加密方式有哪些?🧐

img

双向加密

双向加密是可逆的,多用于数据传输,主要分为对称秘钥加密非对称秘钥加密

  1. 对称秘钥加密

    对称密钥加密,就是采用这种加密方法的双方使用方式用同样的密钥进行加密和解密。密钥是控制加密及解密过程的指令。

    常用的对称加密有:DES、IDEA、RC2、RC4、SKIPJACK、RC5、AES算法等

  2. 非对称秘钥加密

    与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥 (privatekey)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。

    常见的非对称加密有:RSA、DSA算法等

单向加密

单向加密是不可逆的,主要用作密码保存,数据验签等,我们无需知道加密前的数据是什么?只要我们用目标数据加密后和密文比较是否一致。

常见的单向加密有:MD5、SHA算法

本文主要介绍了常见的需要加密处理场景下,如何选择加密方式,主要从以下几个方面介绍:

  • 数据加密储存(MD5+盐值;不可逆,永久有效)
  • 接口验签(SHA+盐值;不可逆,有时效性)
  • 数据加密传输(RSA;数据可逆,公钥加密,私钥解密)

接下来,我们一起看一下各种加密的使用场景和实现方式;

本文涉及到的机密主要用org.springframework.util.DigestUtils类下的方法。

数据加密储存

说到数据加密,最先想到的就是MD5了吧。

    /**
     * MD5加密处理
     *
     * @param password 密码明文
     * @return 加密后密文
     */
    public static String md5(String password) {
        return DigestUtils.md5DigestAsHex(password.getBytes());
    }

123456加密后的密码为:e10adc3949ba59abbe56e057f20f883e

是不是很熟悉的样子,之前接触过一个小项目,放完望去,数据库密密麻麻的全是这个值😂

img

同一个密码加密后,加密后的结果也一致,这样存储数据岂不是也不安全,显然只是单纯的MD5加密并不能满足我们的需求。那我们怎么处理呢?

我们可以尝试添加一个盐值,然后再对数据进行加密,每个用户随机生成一个盐值,然后根据数据+盐的形式加密,然后加密后的数据和盐值都落库,这样就解决了同一个密码加密后的结果也不一样了,我们看一下怎么实现,下面提供了一个实现工具类,供大家参考👇

package com.maple.demo.util.common;

import com.maple.demo.config.bean.ErrorCode;
import com.maple.demo.config.exception.MapleCommonException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;


/**
 * MD5撒盐加密 及MD5加密
 *
 * @author 笑小枫
 * @date 2022/7/20
 * @see <a href="https://www.xiaoxiaofeng.com">https://www.xiaoxiaofeng.com</a>
 */
@Slf4j
public class Md5Util {

    private Md5Util() {

    }

    public static void main(String[] args) {
        String password = "123456";
        log.info("普通MD5加密:" + md5(password));
        String salt = "xxx";
        String cipherText = encrypt(password, salt);
        log.info("MD5加盐加密后的密文:" + cipherText);
        boolean isOk = verifyPassword(password, cipherText, salt);
        log.info("校验密码结果:" + isOk);
    }

    /**
     * MD5加密处理
     *
     * @param password 密码明文
     * @return 加密后密文
     */
    public static String md5(String password) {
        return DigestUtils.md5DigestAsHex(password.getBytes());
    }

    /**
     * 密码加密处理
     *
     * @param password 密码明文
     * @param salt     盐
     * @return 加密后密文
     */
    public static String encrypt(String password, String salt) {
        if (StringUtils.isEmpty(password) || StringUtils.isEmpty(salt)) {
            log.error("密码加密失败原因: password and salt cannot be empty");
            throw new MapleCommonException(ErrorCode.PARAM_ERROR);
        }
        return DigestUtils.md5DigestAsHex((salt + password).getBytes());
    }

    /**
     * 校验密码
     *
     * @param target 待校验密码
     * @param source 原密码
     * @param salt   加密原密码的盐
     */
    public static boolean verifyPassword(String target, String source, String salt) {
        if (StringUtils.isEmpty(target) || StringUtils.isEmpty(source) || StringUtils.isEmpty(salt)) {
            log.info("校验密码失败,原因 target ={}, source ={}, salt ={}", target, source, salt);
            return false;
        }
        String targetEncryptPwd = encrypt(target, salt);
        return targetEncryptPwd.equals(source);
    }
}

这只是一种简单的利用MD5的实现方式,还有很多实现方式,像使用SHA或者自己写加密算法等等,下面我们来看看使用SHA方式加密。

接口验签

接口验签主要用于和第三方接口交互的时候,使用的一种方式,其实就是用不可逆加密的形式加了一个时间的有效期。这里用SHA跟大家演示一下,当然也是可以用上文的MD5的形式。

数据传输格式如下:

  • 签名处理工具类

  • 格式sign_yyyyMMddHHmmss

  • sign:加密后的数据

  • _:用于区分数据,方便后端分割处理,当然也可以使用两个参数传输

  • yyyyMMddHHmmss:当前时间,用于验证签名有效时间

  • 其中sign=SHA256(data&yyyyMMddHHmmss&salt)

  • data:建议放唯一的业务数据

  • yyyyMMddHHmmss:和上文的时间要一致

  • salt:盐值,可随机生产,妥善保管

工具类中用到了自己封装的时间处理工具类,详情见https://www.xiaoxiaofeng.com/archives/javadate

自定义异常可以自己替换成RuntimeException,或者见https://www.xiaoxiaofeng.com/archives/springboot05中自定义异常部分

package com.maple.demo.util.common;

import com.maple.demo.config.bean.ErrorCode;
import com.maple.demo.config.exception.MapleCheckException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;

import java.util.Date;

/**
 * 签名处理工具类
 * 格式sign_yyyyMMddHHmmss
 * sign:加密后的数据
 * _:用于区分数据,方便后端分割处理,当然也可以使用两个参数传输
 * yyyyMMddHHmmss:当前时间,用于验证签名有效时间
 * <p>
 * 其中sign=SHA256(data&yyyyMMddHHmmss&salt)
 * data:建议放唯一的业务数据
 * yyyyMMddHHmmss:和上文的时间要一致
 * salt:盐值,可随机生产,妥善保管
 *
 * @author 笑小枫
 * @date 2022/7/20
 * @see <a href="https://www.xiaoxiaofeng.com">https://www.xiaoxiaofeng.com</a>
 */
@Slf4j
public class SignUtil {

    private SignUtil() {
    }

    public static void main(String[] args) {
        String data = "XXF001";
        String date = DateUtil.dateToStr(new Date(), DateUtil.YYYYMMDDHHMMSS);
        String salt = "d84d9e1fe49d427da66fd78724dcfc29";
        String ciphertext = DigestUtils.sha256Hex(data + "&" + date + "&" + salt);
        log.info("加密后的密文:" + ciphertext);
        checkCangoSign(data, ciphertext + "_" + date, salt, 300L);
    }

    /**
     * 灿谷签名验证
     * 格式sign_yyyyMMddHHmmss
     * 其中sign=sha256Hex(unionId&yyyyMMddHHmmss&salt)
     *
     * @param businessData 业务数据
     * @param sign         签名信息
     * @param salt         加密盐值
     * @param expireSecond 有效期(秒)
     */
    public static void checkCangoSign(String businessData, String sign, String salt, Long expireSecond) {
        if (StringUtils.isBlank(businessData) || StringUtils.isBlank(sign)) {
            log.error(String.format("验签失败,businessData或sign为空,businessData:%s;sign:%s", businessData, sign));
            throw new MapleCheckException(ErrorCode.COMMON_ERROR, "参数有误,请检查后重试");
        }
        String[] signArray = sign.split("_");
        // 签名拆分后为两部分
        int singNum = 2;

        Date reqDate = DateUtil.strToDate(signArray[1], DateUtil.YYYYMMDDHHMMSS);
        Date nowDate = new Date();

        if (reqDate == null || (signArray.length != singNum || secondBetween(reqDate, nowDate) > expireSecond)) {
            log.error(String.format("验签失败,sign无效,businessData:%s;sign:%s", businessData, sign));
            throw new MapleCheckException(ErrorCode.COMMON_ERROR, "sign无效");
        }

        if (secondBetween(nowDate, reqDate) > expireSecond) {
            log.error(String.format("验签失败,sign已过期,businessData:%s;sign:%s", businessData, sign));
            throw new MapleCheckException(ErrorCode.COMMON_ERROR, "sign已过期,请重试");
        }
        String checkDataSha256 = DigestUtils.sha256Hex(businessData + "&" + signArray[1] + "&" + salt);
        if (!signArray[0].equalsIgnoreCase(checkDataSha256)) {
            log.error(String.format("验签失败,签名验证失败,businessData:%s;sign:%s", businessData, sign));
            throw new MapleCheckException(ErrorCode.COMMON_ERROR, "签名验证失败");
        }
    }

    /**
     * 获取两个时间之间相差的秒数
     *
     * @param startDate 开始时间
     * @param endDate   结束时间
     * @return 相差的时间(秒)
     */
    private static long secondBetween(Date startDate, Date endDate) {
        return (startDate.getTime() - endDate.getTime()) / 1000;
    }
}

数据加密传输

什么时候需要数据加密传输呢,比如A系统有一批数据要给B系统,A系统和B系统中没有任何的限制,谁都可以调用,这种场景就需要对接口进行验签操作,对传输的数据进行数据加密,保证数据的安全。

这个时候数据加密就不能使用单向加密了,因为B系统拿到数据后要进行数据解密,所以要用双向加密,这里演示一下非对称的双向加密RSA。

通过生成一对密钥,根据密钥进行数据加密,一般公钥加密,私钥解密,像上文的例子,A系统应该持有公钥加密,B系统持有私钥解密。

img

详细实现见代码👇

package com.maple.demo.util.common;

import com.alibaba.fastjson.JSON;
import com.maple.demo.config.bean.ErrorCode;
import com.maple.demo.config.exception.MapleCommonException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Base64Utils;

import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;

/**
 * RSA加解密处理工具类
 *
 * @author 笑小枫
 * @date 2022/7/20
 * @see <a href="https://www.xiaoxiaofeng.com">https://www.xiaoxiaofeng.com</a>
 */
@Slf4j
public class RsaUtil {

    /**
     * 指定加密算法RSA非对称加密
     */
    private static final String RSA = "RSA";

    /**
     * 指定RSA非对称算法/ECB模式/填充方式
     * 默认填充方式:RSA/ECB/PKCS1Padding
     */
    private static final String RSA_CIPHER = "RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING";

    public static void main(String[] args) {
        Map<String, String> keys = createKeys();
        log.info("密钥对:" + JSON.toJSONString(keys));
        try {
            String ciphertext = publicEncrypt("123abcABC", keys.get("publicKey"));
            log.info("加密后的数据:" + ciphertext);
            String original = privateDecrypt(ciphertext, keys.get("privateKey"));
            log.info("解密后的原文:" + original);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private static Map<String, String> createKeys() {
        //为RSA算法创建一个KeyPairGenerator对象(KeyPairGenerator,密钥对生成器,用于生成公钥和私钥对)
        KeyPairGenerator kpg;
        try {
            kpg = KeyPairGenerator.getInstance(RSA);
        } catch (NoSuchAlgorithmException e) {
            log.error("获取公钥私钥发生错误", e);
            throw new IllegalArgumentException("No such algorithm-->[" + RSA + "]");
        }

        //初始化KeyPairGenerator对象,密钥长度
        kpg.initialize(2048);
        //生成密匙对
        KeyPair keyPair = kpg.generateKeyPair();
        //得到公钥
        Key publicKey = keyPair.getPublic();
        //返回一个publicKey经过二次加密后的字符串
        String publicKeyStr = Base64Utils.encodeToString(publicKey.getEncoded());
        //得到私钥
        Key privateKey = keyPair.getPrivate();
        //返回一个privateKey经过二次加密后的字符串
        String privateKeyStr = Base64Utils.encodeToString(privateKey.getEncoded());

        Map<String, String> keyPairMap = new HashMap<>(16);
        keyPairMap.put("publicKey", publicKeyStr);
        keyPairMap.put("privateKey", privateKeyStr);
        return keyPairMap;
    }

    /**
     * 得到公钥
     *
     * @param publicKey 密钥字符串(经过base64编码)
     */
    private static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
        //通过X509编码的Key指令获得公钥对象
        KeyFactory keyFactory = KeyFactory.getInstance(RSA);
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64Utils.decodeFromString(publicKey));
        return (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
    }

    /**
     * 得到私钥
     *
     * @param privateKey 密钥字符串(经过base64编码)
     */
    private static RSAPrivateKey getPrivateKey(String privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
        //通过PKCS#8编码的Key指令获得私钥对象
        KeyFactory keyFactory = KeyFactory.getInstance(RSA);
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64Utils.decodeFromString(privateKey));
        return (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
    }

    /**
     * 公钥加密
     */
    public static String publicEncrypt(String data, String publicKey) {
        try {
            byte[] dataByte = data.getBytes(StandardCharsets.UTF_8);
            Cipher cipher = Cipher.getInstance(RSA_CIPHER);
            cipher.init(Cipher.ENCRYPT_MODE, getPublicKey(publicKey));
            return Base64Utils.encodeToString(cipher.doFinal(dataByte));
        } catch (Exception e) {
            log.error("加密字符串遇到异常", e);
            throw new MapleCommonException(ErrorCode.COMMON_ERROR, "加密字符串遇到异常");
        }
    }

    /**
     * 私钥解密
     */
    public static String privateDecrypt(String data, String privateKey) {
        try {
            Cipher cipher = Cipher.getInstance(RSA_CIPHER);
            cipher.init(Cipher.DECRYPT_MODE, getPrivateKey(privateKey));
            return new String(cipher.doFinal(Base64Utils.decodeFromString(data)), StandardCharsets.UTF_8);
        } catch (Exception e) {
            log.error("解密字符串遇到异常", e);
            throw new MapleCommonException(ErrorCode.COMMON_ERROR, "解密字符串遇到异常");
        }
    }
}

关于笑小枫💕

本章到这里结束了,喜欢的朋友关注一下我呦😘😘,大伙的支持,就是我坚持写下去的动力。
老规矩,懂了就点赞收藏;不懂就问,日常在线,我会就会回复哈~🤪
微信公众号:笑小枫
笑小枫个人博客:https://www.xiaoxiaofeng.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/641340.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

docker-consul服务发现部署

什么是consul consul是google开源的一个使用go语言开发的服务管理软件。支持多数据中心、分布式高可用的、服务发现和配置共享。采用Raft算法&#xff0c;用来保证服务的高可用。内置了服务注册与发现框架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案&…

Redis 分布式集群操作

文章目录 连接集群写入数据单个key写入批量key操作 集群查询查询 key 的 slot查询 slot 中 key 的数量查询 slot 中的 key 故障转移Master宕机Master 和 Slave 都宕机 集群扩容1.启动两个节点2.添加 master3.分配slot4.添加 slave 集群缩容1.删除 slave 节点2.移出 master 的 s…

【K8SRockyLinux】基于开源操作系统搭建K8S高可用集群(详细版)

文章目录 一、实验节点规划表&#x1f447;二、实验版本说明&#x1f4c3;三、实验拓扑&#x1f4ca;四、实验详细操作步骤&#x1f579;️1. 安装Rocky Linux开源企业操作系统2. 所有主机系统初始化3. 所有master节点部署keepalived4. 所有master节点部署haproxy5. 所有节点配…

睿智医药×企通启动采购与供应链管理项目,加速医药领域数智采购

随着世界经济发展、人口总量增长、人口老龄化程度提高以及人们保健意识增强&#xff0c;新型国家城市化建设的推进和各国医疗保障体制的不断完善&#xff0c;全球医药市场呈持续增长趋势。在政策、资本、技术等因素催化下&#xff0c;我国生物医药行业研发创新实力稳步增强&…

Three.js教程:渲染器

推荐&#xff1a;将 NSDT场景编辑器加入你的3D工具链。 其他系列工具&#xff1a; NSDT简石数字孪生 渲染器 生活中如果有了景物和相机&#xff0c;那么如果想获得一张照片&#xff0c;就需要你拿着相机&#xff0c;按一下&#xff0c;咔&#xff0c;完成拍照。对于threejs而言…

《西部学刊》期刊简介及投稿邮箱

《西部学刊》是经国家新闻出版总署批准&#xff0c;由陕西新华出版传媒集团主管、主办的面向国内外公开发行的综合性哲学社会科学学术期刊。2014年&#xff0c;被国家新闻出版广电总局认定为第一批学术期刊。 《 西部学刊》以全球视野&#xff0c;关注中国西部&#xff0c;聚焦…

webpack生成模式配置

一、生产模式和开发模式介绍 生成模式&#xff08;production mode&#xff09;是指在开发完成后将代码部署到生产环境中运行的模式&#xff0c;通常需要进行代码压缩、优化、合并&#xff0c;以减少文件大小和请求次数&#xff0c;提高页面加载速度和运行效率。 开发模式&am…

生成式人工智能将会对Salesforce的CRM系统护城河构成破坏性威胁

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 第一季度财务业绩 由于Salesforce(CRM)对2024财年的预测挫败了投资者对其人工智能业务前景的信心&#xff0c;所以猛兽财经认为&#xff0c;Salesforce今年的股价反弹可能已经结束了。尽管该公司在第一季度实现了令人印象深…

母婴商家怎么建立自己的品牌,母婴产品传播渠道总结

随着互联网的发展逐渐深入我们的生活&#xff0c;线上传播的模式也越来越被大家熟知。越来越多的行业开始重视线上传播。那么母婴商家怎么建立自己的品牌&#xff0c;母婴产品传播渠道总结。 其实&#xff0c;母婴产品线上用户群体众多&#xff0c;且母婴产品用户目的明确&…

5_普通最小二乘法线性回归案例(Scikit-learn 0.18.2)

现有一批描述家庭用电情况的数据&#xff0c;对数据进行算法模型预测&#xff0c;并最终得到预测模型&#xff08;每天各个时间段和功率之间的关系、功率与电流之间的关系等&#xff09; 数据来源&#xff1a;Individual household electric power consumption Data Set建议&am…

Meta开源音乐生成AI模型MusicGen;直白图解GPT2模型Self Attention注意力机制

&#x1f989; AI新闻 &#x1f680; Meta开源音乐生成AI模型MusicGen 摘要&#xff1a;Meta在Github上开源了其AI语言模型MusicGen&#xff0c;该模型基于Google 2017年推出的Transformer模型&#xff0c;可将文本和旋律转化为完整乐曲。MusicGen支持文本与旋律的组合输入&a…

Python实现ACO蚁群优化算法优化XGBoost回归模型(XGBRegressor算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 蚁群优化算法(Ant Colony Optimization, ACO)是一种源于大自然生物世界的新的仿生进化算法&#xff0c…

AD板子定位孔打孔的两种方式

第一种 注意 直径大小要和 开孔直径一样。 Plated 这个勾打掉 去掉金属壁. 还要X Y坐标 添加一样。 孔位对齐 第二种 选中要开孔的圆 切到机械1层 快捷键 T V B 看效果 总结&#xff1a;第二种最简单

SpringBoot源码-自动装配

一、自动装配原理图 二、入口 springboot的核心注解SpringBootApplication 接着看 SpringBootApplication 注解 截图&#xff1a; 代码&#xff1a; Target({ElementType.TYPE}) //注解的适用范围&#xff0c;Type表示注解可以描述在类、接口、注解或者枚举中 Retention(Ret…

10种常用的数据分析思路

概要 数据分析的思路及其重要&#xff0c;以致于我们总是忽略它&#xff0c;重“术”而轻“道”&#xff0c;但其实应该一视同仁。这篇文章讲了表单分析、用户分析、埋点分析、聚类分析等10种分析方法&#xff0c;先学为敬~ 道家曾强调四个字&#xff0c;叫“道、法、术、器”…

MUR8060PT-ASEMI大电流快恢复二极管80A 600V

编辑&#xff1a;ll MUR8060PT-ASEMI大电流快恢复二极管80A 600V 型号&#xff1a;MUR8060PT 品牌&#xff1a;ASEMI 封装&#xff1a;TO-247 最大漏源电流&#xff1a;80A 漏源击穿电压&#xff1a;600V 引脚数量&#xff1a;2 恢复时间&#xff1a;22ns 正向压降&am…

NIO 基础

3. 文件编程 non-blocking io 非阻塞 IO 1.1 Channel & Buffer channel 类似于 stream&#xff0c;它就是读写数据的双向通道&#xff0c;可以从 channel 将数据读入 buffer&#xff0c;也可以将 buffer 的数据写入 channel&#xff0c;而之前的 stream 要么是输入&#…

经典文献阅读之--RigidFusion(动态障碍物SLAM)

0. 简介 在真实的SLAM场景中&#xff0c;我们会发现在遇到大量动态障碍物的场景时候&#xff0c;特别容易造成跟丢的问题。传统的解决方法是通过将动态障碍物滤除&#xff0c;而本文《RigidFusion: Robot Localisation and Mapping in Environments with Large Dynamic Rigid …

物联网到底如何实现万物互联?

前言&#xff1a;作为计算机相关专业的你&#xff0c;绝对听说过物联网这个词&#xff0c;它的解释相比你也听过&#xff0c;叫万物互联&#xff0c;也就是所谓的IOT&#xff0c;但是说实话它到底如何实现的万物互联的你可能还真不知道。不是每个物体都有一个网络接口或者实体接…

蓝牙客户端QBluetoothSocket的使用——Qt For Android

了解蓝牙 经典蓝牙和低功耗蓝牙差异 经典蓝牙&#xff08;Bluetooth Classic&#xff09;&#xff1a;分为基本速率/增强数据速率(BR/EDR)&#xff0c; 79个信道&#xff0c;在2.4GHz的(ISM)频段。支持点对点设备通信&#xff0c;主要用于实现无线音频流传输&#xff0c;已成…