Spring Boot集成iText实现电子签章

文章目录

  • 一 电子签章
    • 1.1 什么是电子签章
    • 1.2 签名流程
    • 1.3 技术选型
  • 二 实战
    • 2.1 生成数字证书
    • 2.2 生成印章图片
    • 2.3 PDF 签名

一 电子签章

1.1 什么是电子签章

基于《中华人民共和国电子签名法》等相关法规和技术规范,具有法律效力的电子签章一定是需要使用 CA 数字证书进行对文件签名,并把 CA 数字证书存放在签名后文件中。

如果一份签名后的电子文件中无法查看到 CA 数字证书,仅存在一个公章图片,那么就不属于法律意义上的电子签名。电子签名法规定电子文件签署时一定要使用CA数字证书,并没有要求一定需要含有电子印章图片,理论上电子签章不需要到公安局进行备案。

实际上,电子签章是在电子签名技术的基础上添加了印章图像外观,沿袭了人们所习惯的传统盖章可视效果。电子签章使用电子签名技术来保障电子文件内容的防篡改性和签署者的不可否认性。因此,电子签章中,印章图片并不是唯一鉴别是否签章的条件,还要鉴别是否使用高级电子签名技术和 CA 数字证书。

CA 数字证书是在互联网中用于识别身份的一种具有权威性的电子文档。CA 数字证书相当于现实中的身份证。

现实中,如同个人需要去公安局申请办理身份证一样,CA 数字证书需要在“电子认证服务机构”(简称 CA 机构)进行申请办理。中国工业和信息化部、工信部授权 CA 机构来制作、签发数字证书,用非对称加密的方式,生成一对密码即私钥与公开密钥,并绑定了数字证书持有者的真实身份,人们可以在电子合同的缔约过程中用它来证明自己的身份和验证对方的身份。

CA 机构颁发的数字证书为公钥证书和私钥证书:公钥证书是对外公开、任何人都可以使用的,而私钥是专属于签署人所有的。当需要签署文档时,签署人使用私钥证书对电子文件(文档哈希值)进行加密,形成电子签名。 (注:文档哈希值计算时包含待签 PDF 文档内容、印章图片和印章坐标位置信息)

哈希值是指将 PDF 文件按照一定的算法(目前主流是 SHA256 算法),形成一个唯一的文件代码,类似于人类的指纹,任何一个 PDF 文件只有一个哈希值,且不同 PDF 文件的哈希值不可能相同,而相同哈希值的 PDF 文件的内容肯定相同。哈希算法是不可逆的,从哈希值无法推导出 PDF 原文内容。

经签署人的私钥证书加密之后的 PDF 原文哈希值就是电子签名,电子签名中有签署人的姓名、身份证号码、证书有效期、公钥等信息,电子签名放在 PDF 原文的签名域中,就形成了带有电子签名的 PDF 文件。

1.2 签名流程

文件电子签名过程,如下图:

其他人收到这个文件,即可使用PDF文件的签名域中存储的公钥证书对电子签名进行解密,解密出来的文件哈希值如果与原文的哈希值一致,则代表这个文件没有被篡改。

电子签名文件验签过程,如下图:

1.3 技术选型

这块主要有两大技术体系:

  1. 开源组织 Apache 的 PDFBox。
  2. Adobe 的 iText,其中 iText 又分为 iText5 和 iText7。

那么这两个该如何选择呢?

  • PDFBox 的功能相对较弱,iText5 和 iText7 的功能非常强悍。
  • iText5 资料网上相对较多,如果出现问题容易找到解决方案。
  • PDFBox 和 iText7 的网上资料相对较少,如果出现问题不易找到相关解决方案。
  • PDFBox 目前提供的自定义签章接口不完整;而 iText5 和 iText7 提供了处理自定义签章的相关实现。
  • PDFBox 只能实现把签章图片加签到 PDF 文件;iText5 和 iText7 除了可以把签章图片加签到 PDF 文件,还可以实现直接对签章进行绘制,把文件绘制到签章上。
  • PDFBox 和 iText5/iText7 使用的协议不一样。PDFBox 使用的是 APACHE LICENSE VERSION 2.0(Licenses);iText5/iText7 使用的是 AGPL(https://itextpdf.com/agpl)。PDFBox 免费使用,AGPL 商用收费。

因此这里松哥就以 iText5 为例来和小伙伴们演示如何给一个 PDF 文件签名。

二 实战

2.1 生成数字证书

首先我们需要生成一个数字证书。

这个数字证书我们可以利用 JDK 自带的工具生成,为了贴近实战,松哥这里使用 Java 代码生成,生成数字证书的方式如下。

首先引入 Bouncy Castle,Bouncy Castle 是一个广泛使用的开源加密库,它为 Java 平台提供了丰富的密码学算法实现,包括对称加密、非对称加密、哈希算法、数字签名等。这个库由于其广泛的算法支持和可靠性而备受信任,被许多安全应用和加密通信协议所采用

<dependency><groupId>org.bouncycastle</groupId><artifactId>bcpkix-jdk15on</artifactId><version>1.70</version>
</dependency>
<dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-ext-jdk15on</artifactId><version>1.70</version>
</dependency>

接下来我们写一个生成数字证书的工具类,如下:

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;import java.io.*;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.*;/*** @author:江南一点雨* @site:http://www.javaboy.org* @微信公众号:江南一点雨* @github:https://github.com/lenve* @gitee:https://gitee.com/lenve*/
public class PkcsUtils {/*** 生成证书** @return* @throws NoSuchAlgorithmException*/private static KeyPair getKey() throws NoSuchAlgorithmException {KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA",new BouncyCastleProvider());generator.initialize(1024);// 证书中的密钥 公钥和私钥KeyPair keyPair = generator.generateKeyPair();return keyPair;}/*** 生成证书** @param password* @param issuerStr* @param subjectStr* @param certificateCRL* @return*/public static Map<String, byte[]> createCert(String password, String issuerStr, String subjectStr, String certificateCRL) {Map<String, byte[]> result = new HashMap<String, byte[]>();try(ByteArrayOutputStream out= new ByteArrayOutputStream()) {// 标志生成PKCS12证书KeyStore keyStore = KeyStore.getInstance("PKCS12", new BouncyCastleProvider());keyStore.load(null, null);KeyPair keyPair = getKey();// issuer与 subject相同的证书就是CA证书X509Certificate cert = generateCertificateV3(issuerStr, subjectStr,keyPair, result, certificateCRL);// 证书序列号keyStore.setKeyEntry("cretkey", keyPair.getPrivate(),password.toCharArray(), new X509Certificate[]{cert});cert.verify(keyPair.getPublic());keyStore.store(out, password.toCharArray());byte[] keyStoreData = out.toByteArray();result.put("keyStoreData", keyStoreData);return result;} catch (Exception e) {e.printStackTrace();}return result;}/*** 生成证书* @param issuerStr* @param subjectStr* @param keyPair* @param result* @param certificateCRL* @return*/public static X509Certificate generateCertificateV3(String issuerStr,String subjectStr, KeyPair keyPair, Map<String, byte[]> result,String certificateCRL) {ByteArrayInputStream bint = null;X509Certificate cert = null;try {PublicKey publicKey = keyPair.getPublic();PrivateKey privateKey = keyPair.getPrivate();Date notBefore = new Date();Calendar rightNow = Calendar.getInstance();rightNow.setTime(notBefore);// 日期加1年rightNow.add(Calendar.YEAR, 1);Date notAfter = rightNow.getTime();// 证书序列号BigInteger serial = BigInteger.probablePrime(256, new Random());X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(new X500Name(issuerStr), serial, notBefore, notAfter,new X500Name(subjectStr), publicKey);JcaContentSignerBuilder jBuilder = new JcaContentSignerBuilder("SHA1withRSA");SecureRandom secureRandom = new SecureRandom();jBuilder.setSecureRandom(secureRandom);ContentSigner singer = jBuilder.setProvider(new BouncyCastleProvider()).build(privateKey);// 分发点ASN1ObjectIdentifier cRLDistributionPoints = new ASN1ObjectIdentifier("2.5.29.31");GeneralName generalName = new GeneralName(GeneralName.uniformResourceIdentifier, certificateCRL);GeneralNames seneralNames = new GeneralNames(generalName);DistributionPointName distributionPoint = new DistributionPointName(seneralNames);DistributionPoint[] points = new DistributionPoint[1];points[0] = new DistributionPoint(distributionPoint, null, null);CRLDistPoint cRLDistPoint = new CRLDistPoint(points);builder.addExtension(cRLDistributionPoints, true, cRLDistPoint);// 用途ASN1ObjectIdentifier keyUsage = new ASN1ObjectIdentifier("2.5.29.15");// | KeyUsage.nonRepudiation | KeyUsage.keyCertSignbuilder.addExtension(keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment));// 基本限制 X509Extension.javaASN1ObjectIdentifier basicConstraints = new ASN1ObjectIdentifier("2.5.29.19");builder.addExtension(basicConstraints, true, new BasicConstraints(true));X509CertificateHolder holder = builder.build(singer);CertificateFactory cf = CertificateFactory.getInstance("X.509");bint = new ByteArrayInputStream(holder.toASN1Structure().getEncoded());cert = (X509Certificate) cf.generateCertificate(bint);byte[] certBuf = holder.getEncoded();SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");// 证书数据result.put("certificateData", certBuf);//公钥result.put("publicKey", publicKey.getEncoded());//私钥result.put("privateKey", privateKey.getEncoded());//证书有效开始时间result.put("notBefore", format.format(notBefore).getBytes("utf-8"));//证书有效结束时间result.put("notAfter", format.format(notAfter).getBytes("utf-8"));} catch (Exception e) {e.printStackTrace();} finally {if (bint != null) {try {bint.close();} catch (IOException e) {}}}return cert;}public static void main(String[] args) throws Exception {// CN: 名字与姓氏    OU : 组织单位名称// O :组织名称  L : 城市或区域名称  E : 电子邮件// ST: 州或省份名称  C: 单位的两字母国家代码String issuerStr = "CN=javaboy,OU=产品研发部,O=江南一点雨,C=CN,E=javaboy@gmail.com,L=华南,ST=深圳";String subjectStr = "CN=javaboy,OU=产品研发部,O=江南一点雨,C=CN,E=javaboy@gmail.com,L=华南,ST=深圳";String certificateCRL = "http://www.javaboy.org";Map<String, byte[]> result = createCert("123456", issuerStr, subjectStr, certificateCRL);FileOutputStream outPutStream = new FileOutputStream("keystore.p12");outPutStream.write(result.get("keyStoreData"));outPutStream.close();FileOutputStream fos = new FileOutputStream(new File("keystore.cer"));fos.write(result.get("certificateData"));fos.flush();fos.close();}}

运行这个工具代码,会在我们当前工程目录下生成 keystore.p12keystore.cer 两个文件。

其中 keystore.cer 文件通常是一个以 DER 或 PEM 格式存储的 X.509 公钥证书,它包含了公钥以及证书所有者的信息,如姓名、组织、地理位置等。

keystore.p12 文件是一个 PKCS#12 格式的文件,它是一个个人信息交换标准,用于存储一个或多个证书以及它们对应的私钥。.p12 文件是加密的,通常需要密码才能打开。这种文件格式便于将证书和私钥一起分发或存储,常用于需要在不同系统或设备间传输证书和私钥的场景。

总结下就是,.cer 文件通常只包含公钥证书,而 .p12 文件可以包含证书和私钥。

2.2 生成印章图片

接下来我们用 Java 代码绘制一个签章图片,如下:

public class SealSample {public static void main(String[] args) throws Exception {Seal seal = new Seal();seal.setSize(200);SealCircle sealCircle = new SealCircle();sealCircle.setLine(4);sealCircle.setWidth(95);sealCircle.setHeight(95);seal.setBorderCircle(sealCircle);SealFont mainFont = new SealFont();mainFont.setText("江南一点雨股份有限公司");mainFont.setSize(22);mainFont.setFamily("隶书");mainFont.setSpace(22.0);mainFont.setMargin(4);seal.setMainFont(mainFont);SealFont centerFont = new SealFont();centerFont.setText("★");centerFont.setSize(60);seal.setCenterFont(centerFont);SealFont titleFont = new SealFont();titleFont.setText("财务专用章");titleFont.setSize(16);titleFont.setSpace(8.0);titleFont.setMargin(54);seal.setTitleFont(titleFont);seal.draw("公章1.png");}
}

这里涉及到的一些工具类文末可以下载。

最终生成的签章图片类似下面这样:

现在万事具备,可以给 PDF 签名了。

2.3 PDF 签名

最后,我们可以通过如下代码为 PDF 进行签名。

这里我们通过 iText 来实现电子签章,因此需要先引入 iText:

<dependency><groupId>com.itextpdf</groupId><artifactId>itextpdf</artifactId><version>5.5.13.4</version>
</dependency>
<dependency><groupId>com.itextpdf</groupId><artifactId>html2pdf</artifactId><version>5.0.5</version>
</dependency>

接下来对 PDF 文件进行签名:

public class SignPdf2 {/*** @param password pkcs12证书密码* @param keyStorePath pkcs12证书路径* @param signPdfSrc 签名pdf路径* @param signImage 签名图片* @param x* @param y* @return*/public static byte[] sign(String password, String keyStorePath, String signPdfSrc, String signImage,float x, float y) {File signPdfSrcFile = new File(signPdfSrc);PdfReader reader = null;ByteArrayOutputStream signPDFData = null;PdfStamper stp = null;FileInputStream fos = null;try {BouncyCastleProvider provider = new BouncyCastleProvider();Security.addProvider(provider);KeyStore ks = KeyStore.getInstance("PKCS12", new BouncyCastleProvider());fos = new FileInputStream(keyStorePath);// 私钥密码 为Pkcs生成证书是的私钥密码 123456ks.load(fos, password.toCharArray());String alias = (String) ks.aliases().nextElement();PrivateKey key = (PrivateKey) ks.getKey(alias, password.toCharArray());Certificate[] chain = ks.getCertificateChain(alias);reader = new PdfReader(signPdfSrc);signPDFData = new ByteArrayOutputStream();// 临时pdf文件File temp = new File(signPdfSrcFile.getParent(), System.currentTimeMillis() + ".pdf");stp = PdfStamper.createSignature(reader, signPDFData, '\0', temp, true);stp.setFullCompression();PdfSignatureAppearance sap = stp.getSignatureAppearance();sap.setReason("数字签名,不可改变");// 使用png格式透明图片Image image = Image.getInstance(signImage);sap.setImageScale(0);sap.setSignatureGraphic(image);sap.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);// 是对应x轴和y轴坐标sap.setVisibleSignature(new Rectangle(x, y, x + 185, y + 68), 1,UUID.randomUUID().toString().replaceAll("-", ""));stp.getWriter().setCompressionLevel(5);ExternalDigest digest = new BouncyCastleDigest();ExternalSignature signature = new PrivateKeySignature(key, DigestAlgorithms.SHA512, provider.getName());MakeSignature.signDetached(sap, digest, signature, chain, null, null, null, 0, MakeSignature.CryptoStandard.CADES);stp.close();reader.close();return signPDFData.toByteArray();} catch (Exception e) {e.printStackTrace();} finally {if (signPDFData != null) {try {signPDFData.close();} catch (IOException e) {}}if (fos != null) {try {fos.close();} catch (IOException e) {}}}return null;}public static void main(String[] args) throws Exception {byte[] fileData = sign("123456", "keystore.p12", "待签名.pdf",//"公章1.png", 100, 290);FileOutputStream f = new FileOutputStream(new File("已签名.pdf"));f.write(fileData);f.close();}
}

这里所需要的参数基本上前文都提过了,不再多说。

从表面上看,签名结束之后,PDF 文件上多了一个印章,如下:

本质上,则是该 PDF 文件多了一个签名信息,通过 Adobe 的 PDF 软件可以查看,如下:

之所以显示签名有效性未知,是因为我们使用的是自己生成的数字证书,如果从权威机构申请的数字证书,就不会出现这个提示。

好啦,是不是很 easy?

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第5章 中级控件

第 5 章 中级控件 bilibili学习地址 github代码地址 本章介绍App开发常见的几类中级控件的用法&#xff0c;主要包括&#xff1a;如何定制几种简单的图形、如何使用几种选 择按钮、如何高效地输入文本、如何利用对话框获取交互信息等&#xff0c;然后结合本章所学的知识&…

Kubernetes:(四)kubectl命令

文章目录 一、kubectl命令1.查看版本信息 kubectl version2.列出 Kubernetes API 中所有可用的资源及其相关信息 kubectl api-resources3.配置kubectl自动补全 source <(kubectl completion bash)4.查看集群信息 kubectl cluster-info5.node节点查看日志 journalctl -u kube…

互联网人辞职的20条理由,你中了几条?

互联网行业压力大、内卷是众所周知的&#xff0c;想要辞职的念头往往只在一瞬间。 他们想要离职的理由虽然千奇百怪&#xff0c;但每一条都很扎心。 小码在网上搜集了互联网人想要辞职的20条理由&#xff0c;来看看你中了几条吧&#xff1f; 最能戳中你的“辞职理由”是什么呢…

https://huggingface.co/上的模型无法用linux服务器clone怎么办(只需要稍微改一下网址,就可以切换到镜像下载)

问题描述&#xff1a; 在ubuntu系统上&#xff0c;使用如下命令&#xff0c;克隆仓库&#xff0c;报无法访问错误&#xff1a; git clone https://huggingface.co/distilbert/distilroberta-base通用解决方案&#xff1a; 把下面部分更换&#xff1a; https://huggingface.…

使用传感器融合进行3D激光雷达点云运动补偿

此示例展示了如何通过融合来自全球定位系统 (GPS) 和惯性测量单元 (IMU) 传感器的数据来补偿由于自我车辆运动而导致的点云失真。此示例的目标是补偿点云数据中的失真并准确地重新创建周围环境。 文章目录 概述坐标系预处理激光雷达数据预处理 GPS 数据结合 GPS、IMU 和激光雷达…

请看,小白是如何三步速成ComfyUI?

前言 ComfyUI —三步速成秘籍— 嘿&#xff0c;小伙伴们&#xff01; 我是一个刚刚踏入GEO AI实验室的新鲜面孔&#xff0c; 一个对AI设计充满无限好奇的新手。 在这个充满创意和科技感的实验室里&#xff0c; 我只用了短短三个步骤&#xff0c; 就掌握了ComfyUI 。 你…

输电线路火灾隐患监测系统功能与应用是什么?

答&#xff1a;大家好&#xff01;今天我们来聊聊输电线路火灾隐患监测系统TLKS-PMG-DF。这款装置凭借其强大的功能和广泛的应用领域&#xff0c;正在成为电力巡检和山火防控的重要工具。下面&#xff0c;我们就来详细了解一下它的功能与应用吧&#xff01;这款装置配备了先进的…

《高频电子线路》 —— 高频谐振功放

文章内容来源于【中国大学MOOC 华中科技大学通信&#xff08;高频&#xff09;电子线路精品公开课】&#xff0c;此篇文章仅作为笔记分享。 高频谐振功放 主要目的就是功率放大以及高效率。 基本电路原理 高频谐振功放的基本电路&#xff0c;总体上也是由放大管和并联谐振回路…

Java阶段三01

第3章-第1节 一、知识点 maven的简介、安装和使用、仓库管理、项目构建、多模块项目、依赖管理 二、目标 学习了解什么是maven 能够配置maven 使用maven创建项目 掌握maven创建多模块项目的方式 掌握maven的依赖管理和项目构建 三、内容分析 重点 maven的安装和使用 …

【Docker】构建Linux云桌面环境

目录 一、说明 二、离线安装Docker 1&#xff09;将下载的包上传到服务器上去 2&#xff09;安装docker 3) 启动docker 4&#xff09;配置加速器 三、安装云桌面镜像 四、启动云桌面 方式一&#xff1a;docker命令直接运行 方式二&#xff1a;docker-compose方式 五…

【ArcGIS Pro实操第4期】绘制三维地图

【ArcGIS Pro实操第4期】绘制三维地图 ArcGIS Pro绘制三维地图-以DEM高程为例参考 如何使用ArcGIS Pro将栅格数据用三维的形式进行表达&#xff1f;在ArcGIS里可以使用ArcScene来实现&#xff0c;ArcGIS Pro实现原理跟ArcScene一致。由于Esri未来将不再对ArcGIS更新&#xff0c…

“震惊!消费满额即领高额返现,循环购物模式揭秘“

购物满额赠高额返现&#xff0c;每日还能领现金&#xff1f;资金还能提现&#xff1f;这听起来简直像天方夜谭。商家难道真的在无条件发放资金&#xff1f; 大家好&#xff0c;我是电商策略专家吴军。 今天&#xff0c;我要揭秘一种前沿的商业模式——循环消费回馈模式。 这种…

Leetcode 第 420 场周赛题解

Leetcode 第 420 场周赛题解 Leetcode 第 420 场周赛题解题目1&#xff1a;3324. 出现在屏幕上的字符串序列思路代码复杂度分析 题目2&#xff1a;3325. 字符至少出现 K 次的子字符串 I思路代码复杂度分析 题目3&#xff1a;3326. 使数组非递减的最少除法操作次数思路代码复杂度…

Unity3D学习FPS游戏(3)玩家第一人称视角转动和移动

前言&#xff1a;上一篇实现了角色简单的移动控制&#xff0c;但是实际游戏中玩家的视角是可以转动的&#xff0c;并根据转动后视角调整移动正前方。本篇实现玩家第一人称视角转动和移动&#xff0c;觉得有帮助的话可以点赞收藏支持一下&#xff01; 玩家第一人称视角 修复小问…

代码随想录算法训练营第十二天(补) 二叉树| 二叉树理论知识、深度优先遍历、广度优先遍历

目录 一、二叉树理论基础 &#xff08;一&#xff09;二叉树的种类 二叉搜索树 平衡二叉搜索树 &#xff08;二&#xff09;二叉树的存储 &#xff08;三&#xff09;二叉树的遍历 &#xff08;四&#xff09;二叉树的定义 二、二叉树的递归遍历 三、二叉树的层序遍历 …

在线教育系统源码开发详解:网校培训平台搭建的核心技术

本篇文章&#xff0c;笔者将详细介绍在线教育系统源码的开发过程&#xff0c;重点聚焦网校培训平台搭建的核心技术&#xff0c;以期为有意从事在线教育行业的开发者提供实用的参考。 一、在线教育系统的构成 前端负责用户的交互体验&#xff0c;后端处理业务逻辑&#xff0c;…

利士策分享,赚大钱与赚小钱的哲学,选大还是小?

利士策分享&#xff0c;赚大钱与赚小钱的哲学&#xff0c;选大还是小&#xff1f; 在商海浮沉的浪潮中&#xff0c;时常能听到一些业界大佬发表关于财富积累的独到见解。 其中&#xff0c;有一种观点颇为引人注目&#xff0c;那便是“赚大钱比赚小钱容易”。 此言一出&#xf…

【vue】14.插槽:构建可复用组件的关键

今天看代码的时候碰到了插槽&#xff0c;有些看不懂&#xff0c;所以写下这篇文章&#xff0c;系统地梳理一下关于插槽的内容&#xff0c;也希望给大家带来一些帮助。 // 我碰到的插槽长这样 <template #default"scope">... </template> 一.什么是插槽…

Java项目实战II基于Spring Boot的美食烹饪互动平台的设计与实现(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 在当今美食…

ssm基于ssm框架的滁艺咖啡在线销售系统+vue

系统包含&#xff1a;源码论文 所用技术&#xff1a;SpringBootVueSSMMybatisMysql 免费提供给大家参考或者学习&#xff0c;获取源码请私聊我 需要定制请私聊 目 录 第1章 绪论 1 1.1选题动因 1 1.2目的和意义 1 1.3论文结构安排 2 第2章 开发环境与技术 3 2.1 MYSQ…