SpringBoot+Vue实现对称加密和非对称加密

news2024/11/20 0:37:44

我们先来了解一下什么是对称加密和非对称加密,以及两者的优缺点

对称加密

  • 使用同一个密钥对消息进行加密解密

  • 优点:加密和解密的速度快,适合于数据量大的加解密

  • 缺点:密钥在网络传输中可能被泄露,因此安全性相对较低。另外,每对用户每次使用对称加密算法时,都需要使用其他人不知道的独一无二的密钥,这会使得收、发双方所拥有的钥匙数量巨大,密钥管理成为双方的负担

  • 常用的对称加密算法有 DES、3DES、AES、TDEA、Blowfish、RC2、RC4 和 RC5 等。

非对称加密

  • 使用一对密钥进行加密解密,一般公钥用于加密,私钥用于解密

  • 优点:安全性高,因为即使公钥被其他人获取,没有私钥也无法解密

  • 缺点:加密和解密的速度相对较慢

  • 非对称加密在 TLS(或 SSL)协议中得到了广泛应用,HTTPS就是使用的该加密方式

  • 常见的非对称加密算法有 RSA和ECC等。

代码实现

下面我们将从代码层面去实现非对称加密

(我使用的是SpringBoot+Vue2基于RSA实现非对称加密)

前端

1.下载相关依赖

npm install jsencrypt

原生js通过该方式引入

<script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/3.2.1/jsencrypt.min.js"></script>

2.编写相关的工具类

import JSEncrypt from 'jsencrypt';
​
// 密钥对生成 http://web.chacuo.net/netrsakeypair
const publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCj+WHUA1lj2aC9evQuIe/NeIUTgUvvzcZ5EO+msm60tLMdwTag4reQJ/kVLt+41tgHBTBpoxYDBLRDuGsWCbjXvzlJw1rUEgPI+jHwMkE8FrL2HNY1Opz0H3UV7PKhggGV1f2LEia7O08VLY0BbRXcKm9o93flAKt9O9y48nzTDwIDAQAB"
export default {
    // 加密
    encrypt(txt) {
        const encryptor = new JSEncrypt()
        encryptor.setPublicKey(publicKey) // 设置公钥
        return encryptor.encrypt(txt) // 对需要加密的数据进行加密
    },
    // 解密
    decrypt(txt) {
        const encryptor = new JSEncrypt()
        encryptor.setPrivateKey(privateKey)
        return encryptor.decrypt(txt)
    }
}

3.在代码中修改需要加密的相关逻辑(在这里编写了一个简单的登录请求)

<script>
import api from '../api/api'
import encryptPassword from '../utils/encryptPassword'
export default {
    data() {
        return {
            username: 'root',
            password: '123456',
        };
    },
    methods: {
        login() {
            // 使用axios发送登录请求
            try {
                api.post('/user/login', {
                    username: this.username,
                    password: encryptPassword.encrypt(this.password),
                })
                    .then(response => {
                        const data = response.data;
                        if (data.code === -1 && data.msg === '用户名或密码错误') {
                            alert('用户名或密码错误');
                        } else {
                            const token = data.data.token
                            const sessionId = data.data.sessionId
                            localStorage.setItem('token', token);
                            localStorage.setItem('sessionId', sessionId);
                            this.$router.push('/dashbord');
                        }
                    })
            }
            catch (error) {
                // 处理登录失败的情况
                console.error(error);
            };
        }
    },
};
</script>

至此,前端代码已经处理完毕

后端

后端代码编写和前端类似

1.引入相关依赖

<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.15</version>
        </dependency>

2.编写一个RsaUtils工具类

package online.yuuu.fdj.utils;
​
import org.apache.commons.codec.binary.Base64;
import org.springframework.util.ObjectUtils;
import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
​
/**
 * @author Yuuu
 * @description Rsa 工具类,公钥私钥生成,加解密
 **/
public class RsaUtils {
​
    private static final String SRC = "yuuu";
​
    public static void main(String[] args) throws Exception {
        System.out.println("\n");
        RsaKeyPair keyPair = generateKeyPair();
        System.out.println("公钥:" + keyPair.getPublicKey());
        System.out.println("私钥:" + keyPair.getPrivateKey());
        System.out.println("\n");
        test1(keyPair);
        System.out.println("\n");
        test2(keyPair);
        System.out.println("\n");
    }
​
    /**
     * 公钥加密私钥解密
     */
    private static void test1(RsaKeyPair keyPair) throws Exception {
        System.out.println("***************** 公钥加密私钥解密开始 *****************");
        String text1 = encryptByPublicKey(keyPair.getPublicKey(), RsaUtils.SRC);
        String text2 = decryptByPrivateKey(keyPair.getPrivateKey(), text1);
        System.out.println("加密前:" + RsaUtils.SRC);
        System.out.println("加密后:" + text1);
        System.out.println("解密后:" + text2);
        if (RsaUtils.SRC.equals(text2)) {
            System.out.println("解密字符串和原始字符串一致,解密成功");
        } else {
            System.out.println("解密字符串和原始字符串不一致,解密失败");
        }
        System.out.println("***************** 公钥加密私钥解密结束 *****************");
    }
​
    /**
     * 私钥加密公钥解密
     * @throws Exception /
     */
    private static void test2(RsaKeyPair keyPair) throws Exception {
        System.out.println("***************** 私钥加密公钥解密开始 *****************");
        String text1 = encryptByPrivateKey(keyPair.getPrivateKey(), RsaUtils.SRC);
        String text2 = decryptByPublicKey(keyPair.getPublicKey(), text1);
        System.out.println("加密前:" + RsaUtils.SRC);
        System.out.println("加密后:" + text1);
        System.out.println("解密后:" + text2);
        if (RsaUtils.SRC.equals(text2)) {
            System.out.println("解密字符串和原始字符串一致,解密成功");
        } else {
            System.out.println("解密字符串和原始字符串不一致,解密失败");
        }
        System.out.println("***************** 私钥加密公钥解密结束 *****************");
    }
​
    /**
     * 公钥解密
     *
     * @param publicKeyText 公钥
     * @param text 待解密的信息
     * @return /
     * @throws Exception /
     */
    public static String decryptByPublicKey(String publicKeyText, String text) throws Exception {
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, publicKey);
        byte[] result = cipher.doFinal(Base64.decodeBase64(text));
        return new String(result);
    }
​
    /**
     * 私钥加密
     *
     * @param privateKeyText 私钥
     * @param text 待加密的信息
     * @return /
     * @throws Exception /
     */
    public static String encryptByPrivateKey(String privateKeyText, String text) throws Exception {
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);
        byte[] result = cipher.doFinal(text.getBytes());
        return Base64.encodeBase64String(result);
    }
​
    /**
     * 私钥解密
     *
     * @param privateKeyText 私钥
     * @param text 待解密的文本
     * @return /
     * @throws Exception /
     */
    public static String decryptByPrivateKey(String privateKeyText, String text) throws Exception {
        if(ObjectUtils.isEmpty(text)){
            return null;
        }
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] result = cipher.doFinal(Base64.decodeBase64(text));
        return new String(result);
    }
​
    /**
     * 公钥加密
     *
     * @param publicKeyText 公钥
     * @param text 待加密的文本
     * @return /
     */
    public static String encryptByPublicKey(String publicKeyText, String text) throws Exception {
        X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] result = cipher.doFinal(text.getBytes());
        return Base64.encodeBase64String(result);
    }
​
    /**
     * 构建RSA密钥对
     *
     * @return /
     * @throws NoSuchAlgorithmException /
     */
    public static RsaKeyPair generateKeyPair() throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
        String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded());
        String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
        return new RsaKeyPair(publicKeyString, privateKeyString);
    }
​
​
    /**
     * RSA密钥对对象
     */
    public static class RsaKeyPair {
​
        private final String publicKey;
        private final String privateKey;
​
        public RsaKeyPair(String publicKey, String privateKey) {
            this.publicKey = publicKey;
            this.privateKey = privateKey;
        }
​
        public String getPublicKey() {
            return publicKey;
        }
​
        public String getPrivateKey() {
            return privateKey;
        }
​
    }
}

3.修改接口

package online.yuuu.fdj.service.impl;
​
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.IErrorCode;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import online.yuuu.fdj.entity.User;
import online.yuuu.fdj.entity.vo.UserInfo;
import online.yuuu.fdj.mapper.UserMapper;
import online.yuuu.fdj.service.UserService;
import online.yuuu.fdj.utils.RsaUtils;
import online.yuuu.fdj.utils.TokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
​
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
​
/**
 * 用户信息表(User)表服务实现类
 *
 * @author Yuuu
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
​
private static final String PRIVATE_KEY = "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAKP5YdQDWWPZoL169C4h7814hROBS+/NxnkQ76aybrS0sx3BNqDit5An+RUu37jW2AcFMGmjFgMEtEO4axYJuNe/OUnDWtQSA8j6MfAyQTwWsvYc1jU6nPQfdRXs8qGCAZXV/YsSJrs7TxUtjQFtFdwqb2j3d+UAq3073LjyfNMPAgMBAAECgYEAl33bSAkgGQQDYYuq9MQdzPM/31lhUtgGOevPvW60oPchVKghVFqhxOlpUozP3PGA/Ftq8u3ddERwVvCW3vF0d0MlE2MMAs/iqN0riYjep38sSYyFniCtFtaUu8Ukt8A7KzQvAx2eZ5O2S8jV7WXt5yzg2IKmE4cogp/2POP7P6ECQQDe2enNbruDsIJKEhBRWnhSoTeRTZuYZZEWbygcR7VojRsEY/t7dBWwNtB3S+uRwGNTmuushT0pU1Mk2o0at4tVAkEAvF1z+/IO3Z7R2/WQz/AMRxbMSo944caVo1CjGyFJd7pqAsEWcaMDxOjh7nI9XUGhHL63sVcfL3TxAlrjmfkM0wJBALYfEpLf9wDVZHUSNCG4NPMaa+YkrY8rGhpidz49YET0ZGr/Jsuqf/sFoPKNqhPwTXxK9GWGcghu31kT/xMhgw0CQHeQHjHa+KKlz/F9kvjaFwD8sJnXLdSp14u9gHqVC2wq8GVlKJji716Kr/ZJnzfVk62KRVEUyt+1q9Fh3bx7QC8CQQCYlnpD3fsbHNGI8mlIz84nGleysE2cr8zxHpKlyc8hxrjGVT6Crct8o7i4Youa/UUMK09XEl8YflKEln+TOj/o";
    @Override
    public R login(User user, HttpServletRequest request) {
        User one = getOne(new LambdaQueryWrapper<User>().eq(User::getUsername, user.getUsername()));
        try {
            String realPassword = RsaUtils.decryptByPrivateKey(PRIVATE_KEY,user.getPassword());
            if ( one == null || !one.getPassword().equals(realPassword)) {
                return R.failed("用户名或密码错误");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        String token = TokenUtil.generateToken(user.getUsername());
        UserInfo userInfo = new UserInfo();
        userInfo.setId(one.getId());
        userInfo.setUsername(user.getUsername());
        userInfo.setToken(token);
        String sessionId = TokenUtil.storeTokenInSession(request, token);
        userInfo.setSessionId(sessionId);
        return R.ok(userInfo);
    }
}

ok,我们可以验证一下是否完成了加解密

启动一下前后端项目

验证效果

我们可以看到,前端发送的数据已经成功加密

后端接收到的也是加密后的数据

那么我们看一下后端解密的效果,解密也是成功实现了!

题外话

正常企业开发,除了在接发请求时进行数据加密外,还会对入库的数据进行加密,此时的加密就是不可逆的

String realPassword = RsaUtils.decryptByPrivateKey(PRIVATE_KEY,user.getPassword());
            // 拿到真实密码后,使用自带的md5加密算法进行加盐
            String salt = DigestUtils.md5Hex(realPassword);
            // 将(密码 + 盐)再进行加密
            String password = DigestUtils.md5Hex(realPassword + salt);
            // 加密后存入数据库
            one.setPassword(password);

不可逆的加密算法主要包括以下几种:

  • MD5:MD5是一种广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(通常用32字符的十六进制数表示)。然而,MD5被认为在许多安全性方面都存在弱点,因此不建议用于密码加密。

  • HMAC:HMAC(Hash-based Message Authentication Code)是一种基于密钥的哈希算法认证协议。消息认证是用于验证消息的完整性以及保护消息的真实性。

  • SHA系列:SHA(Secure Hash Algorithm)是一种安全哈希算法,包括SHA-1、SHA-224、SHA-256、SHA-384、SHA-512等。其中,SHA-224、SHA-256、SHA-384和SHA-512可以统称为SHA2加密算法。SHA加密算法的安全性要比MD5更高,而SHA2加密算法比SHA1的要高。

这些不可逆加密算法通常用于用户密码加密,其验证过程就是通过比较两个加密后的字符串是否一样来确认身份的。然而,这些加密算法并非绝对安全,例如MD5已经被证明存在安全漏洞,因此在处理敏感信息时,应选择适当的加密算法和策略。在实际应用中,通常会结合对称加密和非对称加密算法,以提高数据的安全性。

我们公司的项目就是先RAS解密,后SHA单向加密

谢谢大家的观看

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

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

相关文章

arcgis javascript api4.x以basetilelayer方式加载天地图web墨卡托(wkid:3857)坐标系

需求&#xff1a; arcgis javascript api4.x以basetilelayer方式加载天地图web墨卡托&#xff08;wkid&#xff1a;3857&#xff09;坐标系 效果图&#xff1a; 代码&#xff1a; 提示&#xff1a; 2个文件放同一个文件夹下 MyCustomTileLayer.js define([exports, "…

手把手教你学会接口自动化系列十一-将用例写在json中,持久化管理起来下

上一篇我写了登录&#xff0c;我们发现json还是没有什么大问题&#xff0c;还蛮好用的&#xff0c;但是我们再写下一个&#xff0c;比如线索新建接口的时候&#xff0c;我们写着写着会发现问题&#xff1a; 我们写获取url的没有问题&#xff0c;代码如下&#xff1a; # !/usr…

uniapp中uview组件库丰富的ActionSheet 操作菜单使用方法

目录 #平台差异说明 #基本使用 #配置顶部的提示信息和底部取消按钮 #如何知道点了第几项 #API #Props #Event 本组件用于从底部弹出一个操作菜单&#xff0c;供用户选择并返回结果。 本组件功能类似于uni的uni.showActionSheetAPI&#xff0c;配置更加灵活&#xff0c;所…

vue:使用【3.0】:拖拽数据

1、参考链接&#xff1a;vue.draggable中文文档 - itxst.com 2、想要实现的效果图&#xff1a;红框内容可以拖拽 3、安装 yarn add vuedraggablenext npm i -S vuedraggablenext 4、代码 <template><draggable:list"columns"ghost-class"ghost&qu…

手把收来教大家win10电脑分辨率怎么调

win10系统操作界面和方式和win7系统有很大不同&#xff0c;有些用户想要设置屏幕的分辨率&#xff0c;但是却不知道应该怎么操作&#xff1f;屏幕分辨率会影响我们使用电脑的视觉效果&#xff0c;经常使用电脑的朋友通常都会设置一个合适的分辨率。下面小编来教大家win10电脑分…

webpack的性能优化(一)——分包优化

1.什么是分包&#xff1f;为什么要分包&#xff1f; 默认情况下&#xff0c;Webpack 会将所有代码构建成一个单独的包&#xff0c;这在小型项目通常不会有明显的性能问题&#xff0c;但伴随着项目的推进&#xff0c;包体积逐步增长可能会导致应用的响应耗时越来越长。归根结底这…

什么是google算法?

谷歌算法本身指的是谷歌针对搜索引擎做的规定 要想在别人的地盘玩&#xff0c;那肯定要了解这个地盘的规定&#xff0c;不然做了什么违反了规定&#xff0c;谷歌肯定不会让你继续玩下去 要想做谷歌&#xff0c;那肯定要了解谷歌的算法&#xff0c;然而谷歌的算法也不是一成不变…

代码随想录 Leetcode202. 快乐数

题目&#xff1a; 代码(首刷自解 2024年1月15日&#xff09;&#xff1a; class Solution { public:bool isHappy(int n) {unordered_set<int> hash;while(n ! 1) {int sum 0;while(n/10 ! 0) {sum (n % 10)*(n % 10);n/10;}sum n*n;if (hash.find(sum) ! hash.end()…

SQL备忘--集合运算

前言 本文讨论的是两个子查询结果的合并问题&#xff0c; 是行维度下的合并处理 例如子查询A查出5条记录、子查询B查出3条记录&#xff0c;那么将两个结果合并&#xff0c;则共返回8条记录 行维度上要能进行合并&#xff0c;前置要求是&#xff1a;子查询的列字段是相同的&…

关于你不知道的前端文件上传方式

1、原生方法 我们开发中比较常用的有使用 input 属性的 type 设置为 file,这里我们就不进行过多的阐述&#xff0c;已经老生常谈了。 今天我们主要介绍两个新的属性 showDirectoryPicker 和 showOpenFilePicker 这里是官方文档 1、showDirectoryPicker showDirectoryPicker方…

20240115-【UNITY 学习】第一人称移动增加斜坡移动、冲刺和蹲伏功能

直接修改或者替换PlayerMovement_01.cs using System.Collections; using System.Collections.Generic; using UnityEngine;public class PlayerMovement_02 : MonoBehaviour {private float moveSpeed; // 玩家移动速度public float walkSpeed 7; // 行走速度public float sp…

内网搭建文件服务器

文件下载地址 Releases rejetto/hfs GitHubHFS is a web file server to run on your computer. Share folders or even a single file thanks to the virtual file system. - Releases rejetto/hfshttps://github.com/rejetto/hfs/releases http://192.168.100.33/apk/ 就…

.net core IResultFilter 的 OnResultExecuted和OnResultExecuting的区别

//全局过滤器 builder.Services.AddMvc(m > { m.Filters.Add<AllResultFilter>(); }); 1、实现过滤器 public class AllResultFilter : IResultFilter {/// <summary>/// 结果执行后方法/// 不可更改结果/// </summary>/// <param name"con…

spring常见漏洞(2)

Spring Web Flow框架远程代码执行(CVE-2017-4971)漏洞&#xff0c;是由于Spring Web Flow的数据绑定问题带来的表达式注入&#xff0c;从而导致任意代码执行。 影响版本 2.4.0-2.4.4、Older unsupported versions are also affected 漏洞分析 view对象处理用户事件&#xf…

【外汇天眼】误入假冒Ctrl Investments无法出金,投资者:太相信网友了!

在当下这个互联网迅速发展的时代&#xff0c;各类交友类APP成为人们拓展社交圈的新渠道。一方面这样的交友软件在满足了用户基础的社交要求&#xff0c;另一方面网络世界所交往的朋友能给用户带来的神秘感和新鲜感&#xff0c;所以导致一部分年轻人离不开这些交友软件。然而&am…

STM32快速复制MX25L1606E系列Flash

去年做了一个使用RS485对PIC18F45K80系列单片机进行在线升级的程序&#xff0c;如果是小批量的出厂烧录程序和升级验证&#xff08;出厂前肯定要测试单片机是否能正常读写Flash&#xff09;是可以的&#xff0c;但是后来产品订单量很大&#xff0c;生产线的烧录及升级验证就很缓…

QT上位机开发(多线程处理)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 目前大部分cpu都是多核cpu&#xff0c;所以如果不用多线程进行数据处理的话&#xff0c;实在是太浪费资源了。另外&#xff0c;很多的操作都是阻塞…

数据中心温湿度监测,这个细节你真的看懂了吗?

在当今数字化和智能化的时代&#xff0c;温湿度监控系统成为了各行业中不可或缺的关键技术&#xff0c;其在维护设备、保障产品质量和确保生产环境稳定性方面发挥着至关重要的作用。 随着科技的不断进步&#xff0c;企业和机构越来越意识到对环境参数的实时监测对于提高效率、降…

采样次数与频率的关系

采样次数&#xff08;Sampling Points&#xff09; 在给定时间内记录信号值的次数。 假设在1秒内对一个连续信号采样10次&#xff0c;这意味着每0.1秒记录一次信号值。 假设在1秒内对一个连续信号采样100次&#xff0c;这意味着每0.01秒记录一次信号值。 频率&#xff08;Fre…

GitLab CI 实现项目A更新代码自动触发项目B更新错误码文档

一、CI/CD简介 CI/CD 是持续集成&#xff08;Continuous Integration&#xff09;和持续交付/持续部署&#xff08;Continuous Delivery/Continuous Deployment&#xff09;的缩写&#xff0c;是一种软件开发和交付的最佳实践。这两个概念通常一起使用&#xff0c;但有些时候它…