热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

SpringBoot文件分片上传教程_java

这篇文章主要介绍了SpringBoot文件分片上传教程,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完

背景

最近好几个项目在运行过程中客户都提出文件上传大小的限制能否设置的大一些,用户经常需要上传好几个G的资料文件,如图纸,视频等,并且需要在上传大文件过程中进行优化实时展现进度条,进行技术评估后针对框架文件上传进行扩展升级,扩展接口支持大文件分片上传处理,减少服务器瞬时的内存压力,同一个文件上传失败后可以从成功上传分片位置进行断点续传,文件上传成功后再次上传无需等待达到秒传的效果,优化用户交互体验

具体的实现流程如下图所示

文件MD5计算

对于文件md5的计算我们使用spark-md5第三方库,大文件我们可以分片分别计算再合并节省时间,但是经测试1G文件计算MD5需要20s左右的时间,所以经过优化我们抽取文件部分特征信息(文件第一片+文件最后一片+文件修改时间),来保证文件的相对唯一性,只需要2s左右,大大提高前端计算效率,对于前端文件内容块的读取我们需要使用html5的api中fileReader.readAsArrayBuffer方法,因为是异步触发,封装的方法提供一个回调函数进行使用

createSimpleFileMD5(file, chunkSize, finishCaculate) {
var fileReader = new FileReader();
var blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice;
var chunks = Math.ceil(file.size / chunkSize);
var currentChunk = 0;
var spark = new SparkMD5.ArrayBuffer();
var startTime = new Date().getTime();
loadNext();
fileReader.Onload= function() {
spark.append(this.result);
if (currentChunk == 0) {
currentChunk = chunks - 1;
loadNext();
} else {
var fileMD5 = hpMD5(spark.end() + file.lastModifiedDate);
finishCaculate(fileMD5)
}
};
function loadNext() {
var start = currentChunk * chunkSize;
var end = start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
}

文件分片切割

我们通过定义好文件分片大小,使用blob对象支持的file.slice方法切割文件,分片上传请求需要同步按顺序请求,因为使用了同步请求,前端ui会阻塞无法点击,需要开启worker线程进行操作,完成后通过postMessage方法传递消息给主页面通知ui进度条的更新,需要注意的是,worker线程方法不支持window对象,所以尽量不要使用第三方库,使用原生的XMLHttpRequest对象发起请求,需要的参数通过onmessage方法传递获取

页面upload请求方法如下

upload() {
var file = document.getElementById("file").files[0];
if (!file) {
alert("请选择需要上传的文件");
return;
}
if (file.size alert("选择的文件请大于" + pageData.chunkSize / 1024 / 1024 + "M")
}
var filesize = file.size;
var filename = file.name;
pageData.chunkCount = Math.ceil(filesize / pageData.chunkSize);
this.createSimpleFileMD5(file, pageData.chunkSize, function(fileMD5) {
console.log("计算文件MD:" + fileMD5);
pageData.showProgress = true;
var worker = new Worker('worker.js');
var param = {
token: GetTokenID(),
uploadUrl: uploadUrl,
filename: filename,
filesize: filesize,
fileMD5: fileMD5,
groupguid: pageData.groupguid1,
grouptype: pageData.grouptype1,
chunkCount: pageData.chunkCount,
chunkSize: pageData.chunkSize,
file: file
}
worker.Onmessage= function(event) {
var workresult = event.data;
if (workresult.code == 0) {
pageData.percent = workresult.percent;
if (workresult.percent == 100) {
pageData.showProgress = false;
worker.terminate();
}
} else {
pageData.showProgress = false;
worker.terminate();
}
}
worker.postMessage(param);
})
}

worker.js执行方法如下

function FormAjax_Sync(token, data, url, success) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("post", url, false);
xmlHttp.setRequestHeader("token", token);
xmlHttp.Onreadystatechange= function() {
if (xmlHttp.status == 200) {
var result = JSON.parse(this.responseText);
var status = this.status
success(result, status);
}
};
xmlHttp.send(data);
}
Onmessage= function(evt) {
var data = evt.data;
console.log(data)
//传递的参数
var token = data.token
var uploadUrl = data.uploadUrl
var filename = data.filename
var fileMD5 = data.fileMD5
var groupguid = data.groupguid
var grouptype = data.grouptype
var chunkCount = data.chunkCount
var chunkSize = data.chunkSize
var filesize = data.filesize
var filename = data.filename
var file = data.file
var start = 0;
var end;
var index = 0;
var startTime = new Date().getTime();
while (start end = start + chunkSize;
if (end > filesize) {
end = filesize;
}
var chunk = file.slice(start, end); //切割文件
var formData = new FormData();
formData.append("file", chunk, filename);
formData.append("fileMD5", fileMD5);
formData.append("chunkCount", chunkCount)
formData.append("chunkIndex", index);
formData.append("chunkSize", end - start);
formData.append("groupguid", groupguid);
formData.append("grouptype", grouptype);
//上传文件
FormAjax_Sync(token, formData, uploadUrl, function(result, status) {
var code = 0;
var percent = 0;
if (result.code == 0) {
console.log("分片共" + chunkCount + "个" + ",已成功上传第" + index + "个")
percent = parseInt((parseInt(formData.get("chunkIndex")) + 1) * 100 / chunkCount);
} else {
filesize = -1;
code = -1
console.log("分片第" + index + "个上传失败")
}
self.postMessage({ code: code, percent: percent });
})
start = end;
index++;
}
console.log("上传分片总时间:" + (new Date().getTime() - startTime));
console.log("分片完成");
}

文件分片接收

前端文件分片处理完毕后,接下来我们详细介绍下后端文件接受处理的方案,分片处理需要支持用户随时中断上传与文件重复上传,我们新建表f_attachchunk来记录文件分片的详细信息,

表结构设计如下:

CREATE TABLE `f_attachchunk` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`ChunkGuid` varchar(50) NOT NULL,
`FileMD5` varchar(100) DEFAULT NULL,
`FileName` varchar(200) DEFAULT NULL,
`ChunkSize` int(11) DEFAULT NULL,
`ChunkCount` int(11) DEFAULT NULL,
`ChunkIndex` int(11) DEFAULT NULL,
`ChunkFilePath` varchar(500) DEFAULT NULL,
`UploadUserGuid` varchar(50) DEFAULT NULL,
`UploadUserName` varchar(100) DEFAULT NULL,
`UploadDate` datetime DEFAULT NULL,
`UploadOSSID` varchar(200) DEFAULT NULL,
`UploadOSSChunkInfo` varchar(1000) DEFAULT NULL,
`ChunkType` varchar(50) DEFAULT NULL,
`MergeStatus` int(11) DEFAULT NULL,
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=237 DEFAULT CHARSET=utf8mb4;




  • FileMD5:文件MD5唯一标识文件


  • FileName:文件名称


  • ChunkSize:分片大小


  • ChunkCount:分片总数量


  • ChunkIndex:分片对应序号


  • ChunkFilePath:分片存储路径(本地存储文件方案使用)


  • UploadUserGuid:上传人主键


  • UploadUserName:上传人姓名


  • UploadDate:上传人日期


  • UploadOSSID:分片上传批次ID(云存储方案使用)


  • UploadOSSChunkInfo:分片上传单片信息(云存储方案使用)


  • ChunkType:分片存储方式(本地存储,阿里云,华为云,Minio标识)


  • MergeStatus:分片合并状态(未合并,已合并)

文件分片存储后端一共分为三步,检查分片=》保存分片=》合并分片,我们这里先以本地文件存储为例讲解,云存储思路一致,后续会提供对应使用的api方法

检查分片

检查分片以数据库文件分片记录的FIleMD5与ChunkIndex组合来确定分片的唯一性,因为本地分片temp文件是作为临时文件存储,可能会出现手动清除施放磁盘空间的问题,所以数据库存在记录我们还需要对应的检查实际文件情况

boolean existChunk = false;
AttachChunkDO dbChunk = attachChunkService.checkExistChunk(fileMD5, chunkIndex, "Local");
if (dbChunk != null) {
File chunkFile = new File(dbChunk.getChunkFilePath());
if (chunkFile.exists()) {
if (chunkFile.length() == chunkSize) {
existChunk = true;
} else {
//删除数据库记录
attachChunkService.delete(dbChunk.getChunkGuid());
}
} else {
//删除数据库记录
attachChunkService.delete(dbChunk.getChunkGuid());
}
}

保存分片

保存分片分为两块,文件存储到本地,成功后数据库插入对应分片信息

//获取配置中附件上传文件夹
String filePath = frameConfig.getAttachChunkPath() + "/" + fileMD5 + "/";
//根据附件guid创建文件夹
File targetFile = new File(filePath);
if (!targetFile.exists()) {
targetFile.mkdirs();
}
if (!existChunk) {
//保存文件到文件夹
String chunkFileName = fileMD5 + "-" + chunkIndex + ".temp";
FileUtil.uploadFile(FileUtil.convertStreamToByte(fileContent), filePath, chunkFileName);
//插入chunk表
AttachChunkDO attachChunkDO = new AttachChunkDO(fileMD5, fileName, chunkSize, chunkCount, chunkIndex, filePath + chunkFileName, "Local");
attachChunkService.insert(attachChunkDO);
}

合并分片

在上传分片方法中,如果当前分片是最后一片,上传完毕后进行文件合并工作,同时进行数据库合并状态的更新,下一次同一个文件上传时我们可以直接拷贝之前合并过的文件作为新附件,减少合并这一步骤的I/O操作,合并文件我们采用BufferedOutputStream与BufferedInputStream两个对象,固定缓冲区大小

if (chunkIndex == chunkCount - 1) {
//合并文件
String merageFileFolder = frameConfig.getAttachPath() + groupType + "/" + attachGuid;
File attachFolder = new File(merageFileFolder);
if (!attachFolder.exists()) {
attachFolder.mkdirs();
}
String merageFilePath = merageFileFolder + "/" + fileName;
merageFile(fileMD5, merageFilePath);
attachChunkService.updateMergeStatusToFinish(fileMD5);
//插入到附件库
//设置附件唯一guid
attachGuid = CommonUtil.getNewGuid();
attachmentDO.setAttguid(attachGuid);
attachmentService.insert(attachmentDO);
}


public void merageFile(String fileMD5, String targetFilePath) throws Exception {
String merageFilePath = frameConfig.getAttachChunkPath()+"/"+fileMD5+"/"+fileMD5+".temp";
File merageFile = new File(merageFilePath);
if(!merageFile.exists()){
BufferedOutputStream destOutputStream = new BufferedOutputStream(new FileOutputStream(merageFilePath));
List attachChunkDOList = attachChunkService.selectListByFileMD5(fileMD5, "Local");
for (AttachChunkDO attachChunkDO : attachChunkDOList) {
File file = new File(attachChunkDO.getChunkFilePath());
byte[] fileBuffer = new byte[1024 * 1024 * 5];//文件读写缓存
int readBytesLength = 0; //每次读取字节数
BufferedInputStream sourceInputStream = new BufferedInputStream(new FileInputStream(file));
while ((readBytesLength = sourceInputStream.read(fileBuffer)) != -1) {
destOutputStream.write(fileBuffer, 0, readBytesLength);
}
sourceInputStream.close();
}
destOutputStream.flush();
destOutputStream.close();
}
FileUtil.copyFile(merageFilePath,targetFilePath);
}

云文件分片上传

云文件上传与本地文件上传的区别就是,分片文件直接上传到云端,再调用云存储api进行文件合并与文件拷贝,数据库相关记录与检查差异不大

阿里云OSS

上传分片前需要生成该文件的分片上传组标识uploadid

public String getUplaodOSSID(String key){
key = "chunk/" + key + "/" + key;
TenantParams.attach appCOnfig= getAttach();
OSSClient ossClient = InitOSS(appConfig);
String bucketName = appConfig.getBucketname_auth();
InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key);
InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request);
String uploadId = upresult.getUploadId();
ossClient.shutdown();
return uploadId;
}

上传分片时需要指定uploadid,同时我们要将返回的分片信息PartETag序列化保存数据库,用于后续的文件合并

public String uploadChunk(InputStream stream,String key, int chunkIndex, int chunkSize, String uploadId){
key = "chunk/" + key + "/" + key;
String result = "";
try{
TenantParams.attach appCOnfig= getAttach();
OSSClient ossClient = InitOSS(appConfig);
String bucketName = appConfig.getBucketname_auth();
UploadPartRequest uploadPartRequest = new UploadPartRequest();
uploadPartRequest.setBucketName(bucketName);
uploadPartRequest.setKey(key);
uploadPartRequest.setUploadId(uploadId);
uploadPartRequest.setInputStream(stream);
// 设置分片大小。除了最后一个分片没有大小限制,其他的分片最小为100 KB。
uploadPartRequest.setPartSize(chunkSize);
// 设置分片号。每一个上传的分片都有一个分片号,取值范围是1~10000,如果超出此范围,OSS将返回InvalidArgument错误码。
uploadPartRequest.setPartNumber(chunkIndex+1);
// 每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会按照分片号排序组成完整的文件。
UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
PartETag partETag = uploadPartResult.getPartETag();
result = JSON.toJSONString(partETag);
ossClient.shutdown();
}catch (Exception e){
logger.error("OSS上传文件Chunk失败:" + e.getMessage());
}
return result;
}

合并分片时通过传递保存分片的PartETag对象数组进行操作,为了附件独立唯一性我们不直接使用合并后的文件,通过api进行文件拷贝副本使用

public boolean merageFile(String uploadId, List chunkInfoList,String key,AttachmentDO attachmentDO,boolean checkMerge){
key = "chunk/" + key + "/" + key;
boolean result = true;
try{
TenantParams.attach appCOnfig= getAttach();
OSSClient ossClient = InitOSS(appConfig);
String bucketName = appConfig.getBucketname_auth();
if(!checkMerge){
CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest(bucketName, key, uploadId, chunkInfoList);
CompleteMultipartUploadResult completeMultipartUploadResult = ossClient.completeMultipartUpload(completeMultipartUploadRequest);
}
String attachKey = getKey(attachmentDO);
ossClient.copyObject(bucketName,key,bucketName,attachKey);
ossClient.shutdown();
}catch (Exception e){
e.printStackTrace();
logger.error("OSS合并文件失败:" + e.getMessage());
result = false;
}
return result;
}

华为云OBS

华为云api与阿里云api大致相同,只有个别参数名称不同,直接上代码

public String getUplaodOSSID(String key) throws Exception {
key = "chunk/" + key + "/" + key;
TenantParams.attach appCOnfig= getAttach();
ObsClient obsClient = InitOBS(appConfig);
String bucketName = appConfig.getBucketname_auth();
InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key);
InitiateMultipartUploadResult result = obsClient.initiateMultipartUpload(request);
String uploadId = result.getUploadId();
obsClient.close();
return uploadId;
}
public String uploadChunk(InputStream stream, String key, int chunkIndex, int chunkSize, String uploadId) {
key = "chunk/" + key + "/" + key;
String result = "";
try {
TenantParams.attach appCOnfig= getAttach();
ObsClient obsClient = InitOBS(appConfig);
String bucketName = appConfig.getBucketname_auth();
UploadPartRequest uploadPartRequest = new UploadPartRequest();
uploadPartRequest.setBucketName(bucketName);
uploadPartRequest.setUploadId(uploadId);
uploadPartRequest.setObjectKey(key);
uploadPartRequest.setInput(stream);
uploadPartRequest.setOffset(chunkIndex * chunkSize);
// 设置分片大小。除了最后一个分片没有大小限制,其他的分片最小为100 KB。
uploadPartRequest.setPartSize((long) chunkSize);
// 设置分片号。每一个上传的分片都有一个分片号,取值范围是1~10000,如果超出此范围,OSS将返回InvalidArgument错误码。
uploadPartRequest.setPartNumber(chunkIndex + 1);
// 每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会按照分片号排序组成完整的文件。
UploadPartResult uploadPartResult = obsClient.uploadPart(uploadPartRequest);
PartEtag partETag = new PartEtag(uploadPartResult.getEtag(), uploadPartResult.getPartNumber());
result = JSON.toJSONString(partETag);
obsClient.close();
} catch (Exception e) {
e.printStackTrace();
logger.error("OBS上传文件Chunk:" + e.getMessage());
}
return result;
}
public boolean merageFile(String uploadId, List chunkInfoList, String key, AttachmentDO attachmentDO, boolean checkMerge) {
key = "chunk/" + key + "/" + key;
boolean result = true;
try {
TenantParams.attach appCOnfig= getAttach();
ObsClient obsClient = InitOBS(appConfig);
String bucketName = appConfig.getBucketname_auth();
if (!checkMerge) {
CompleteMultipartUploadRequest request = new CompleteMultipartUploadRequest(bucketName, key, uploadId, chunkInfoList);
obsClient.completeMultipartUpload(request);
}
String attachKey = getKey(attachmentDO);
obsClient.copyObject(bucketName, key, bucketName, attachKey);
obsClient.close();
} catch (Exception e) {
e.printStackTrace();
logger.error("OBS合并文件失败:" + e.getMessage());
result = false;
}
return result;
}

Minio

文件存储Minio应用比较广泛,框架也同时支持了自己独立部署的Minio文件存储系统,Minio没有对应的分片上传api支持,我们可以在上传完分片文件后,使用composeObject方法进行文件的合并

public boolean uploadChunk(InputStream stream, String key, int chunkIndex) {
boolean result = true;
try {
MinioClient minioClient = InitMinio();
String bucketName = frameConfig.getMinio_bucknetname();
PutObjectOptions option = new PutObjectOptions(stream.available(), -1);
key = "chunk/" + key + "/" + key;
minioClient.putObject(bucketName, key + "-" + chunkIndex, stream, option);
} catch (Exception e) {
logger.error("Minio上传Chunk文件失败:" + e.getMessage());
result = false;
}
return result;
}
public boolean merageFile(String key, int chunkCount, AttachmentDO attachmentDO, boolean checkMerge) {
boolean result = true;
try {
MinioClient minioClient = InitMinio();
String bucketName = frameConfig.getMinio_bucknetname();
key = "chunk/" + key + "/" + key;
if (!checkMerge) {
List sourceObjectList = new ArrayList();
for (int i = 0; i ComposeSource composeSource = ComposeSource.builder().bucket(bucketName).object(key + "-" + i).build();
sourceObjectList.add(composeSource);
}
minioClient.composeObject(ComposeObjectArgs.builder().bucket(bucketName).object(key).sources(sourceObjectList).build());
}
String attachKey = getKey(attachmentDO);
minioClient.copyObject(
CopyObjectArgs.builder()
.bucket(bucketName)
.object(attachKey)
.source(
CopySource.builder()
.bucket(bucketName)
.object(key)
.build())
.build());
} catch (Exception e) {
logger.error("Minio合并文件失败:" + e.getMessage());
result = false;
}
return result;
}


推荐阅读
  • 2018年9月21日,Destoon官方发布了安全更新,修复了一个由用户“索马里的海贼”报告的前端GETShell漏洞。该漏洞存在于20180827版本的某CMS中,攻击者可以通过构造特定的HTTP请求,利用该漏洞在服务器上执行任意代码,从而获得对系统的控制权。此次更新建议所有用户尽快升级至最新版本,以确保系统的安全性。 ... [详细]
  • 网站访问全流程解析
    本文详细介绍了从用户在浏览器中输入一个域名(如www.yy.com)到页面完全展示的整个过程,包括DNS解析、TCP连接、请求响应等多个步骤。 ... [详细]
  • 在JavaWeb开发中,文件上传是一个常见的需求。无论是通过表单还是其他方式上传文件,都必须使用POST请求。前端部分通常采用HTML表单来实现文件选择和提交功能。后端则利用Apache Commons FileUpload库来处理上传的文件,该库提供了强大的文件解析和存储能力,能够高效地处理各种文件类型。此外,为了提高系统的安全性和稳定性,还需要对上传文件的大小、格式等进行严格的校验和限制。 ... [详细]
  • 在PHP中如何正确调用JavaScript变量及定义PHP变量的方法详解 ... [详细]
  • 【实例简介】本文详细介绍了如何在PHP中实现微信支付的退款功能,并提供了订单创建类的完整代码及调用示例。在配置过程中,需确保正确设置相关参数,特别是证书路径应根据项目实际情况进行调整。为了保证系统的安全性,存放证书的目录需要设置为可读权限。值得注意的是,普通支付操作无需证书,但在执行退款操作时必须提供证书。此外,本文还对常见的错误处理和调试技巧进行了说明,帮助开发者快速定位和解决问题。 ... [详细]
  • Keepalived 提供了多种强大且灵活的后端健康检查机制,包括 HTTP_GET、SSL_GET、TCP_CHECK、SMTP_CHECK 和 MISC_CHECK 等多种检测方法。这些健康检查功能确保了高可用性环境中的服务稳定性和可靠性。通过合理配置这些检查方式,可以有效监测后端服务器的状态,及时发现并处理故障,从而提高系统的整体性能和可用性。 ... [详细]
  • 深入探索HTTP协议的学习与实践
    在初次访问某个网站时,由于本地没有缓存,服务器会返回一个200状态码的响应,并在响应头中设置Etag和Last-Modified等缓存控制字段。这些字段用于后续请求时验证资源是否已更新,从而提高页面加载速度和减少带宽消耗。本文将深入探讨HTTP缓存机制及其在实际应用中的优化策略,帮助读者更好地理解和运用HTTP协议。 ... [详细]
  • 在PHP中实现腾讯云接口签名,以完成人脸核身功能的对接与签名配置时,需要注意将文档中的POST请求改为GET请求。具体步骤包括:使用你的`secretKey`生成签名字符串`$srcStr`,格式为`GET faceid.tencentcloudapi.com?`,确保参数正确拼接,避免因请求方法错误导致的签名问题。此外,还需关注API的其他参数要求,确保请求的完整性和安全性。 ... [详细]
  • 本文详细解析了微信服务端示例类的功能与应用。其中,`ClientResponseHandler` 类主要用于处理微信支付所需的响应数据,而 `TenpayHttpClient` 则是对 HTTP 请求(包括 GET 和 POST 方法)进行了封装,以便在内部调用时更加便捷和高效。这些工具类在实际开发中起到了关键作用,开发者无需深入了解其底层实现细节,即可轻松集成微信支付功能。 ... [详细]
  • FastDFS Nginx 扩展模块的源代码解析与技术剖析
    FastDFS Nginx 扩展模块的源代码解析与技术剖析 ... [详细]
  • 本文探讨了SMTP AUTH扩展的问题及其在Python中的应用解决方案。通过分析SMTP协议的安全性不足,提出了使用SMTP AUTH扩展来增强邮件传输的安全性。文章详细介绍了SMTP AUTH的工作原理,并结合Python编程语言,提供了一种实现SMTP AUTH认证的方法。此外,还讨论了常见的实现问题及解决策略,为开发者提供了实用的参考。 ... [详细]
  • React 实现 Post 请求下载 PDF 文件的解决方案
    在 React 应用中实现通过 POST 请求下载 PDF 文件的功能,本文提供了完整的代码示例。具体实现包括设置状态以显示加载提示,并通过控制台日志记录下载索引,确保请求的正确性和用户体验。此外,还详细介绍了如何处理响应流并将其转换为可下载的 PDF 文件,适用于需要安全传输数据的场景。 ... [详细]
  • 解决Bootstrap DataTable Ajax请求重复问题
    在最近的一个项目中,我们使用了JQuery DataTable进行数据展示,虽然使用起来非常方便,但在测试过程中发现了一个问题:当查询条件改变时,有时查询结果的数据不正确。通过FireBug调试发现,点击搜索按钮时,会发送两次Ajax请求,一次是原条件的请求,一次是新条件的请求。 ... [详细]
  • 秒建一个后台管理系统?用这5个开源免费的Java项目就够了
    秒建一个后台管理系统?用这5个开源免费的Java项目就够了 ... [详细]
  • 深入解析HTTPS:保障Web安全的加密协议
    本文详细探讨了HTTPS协议在保障Web安全中的重要作用。首先分析了HTTP协议的不足之处,包括数据传输过程中的安全性问题和内容加密的缺失。接着介绍了HTTPS如何通过使用公钥和私钥的非对称加密技术以及混合加密机制,确保数据的完整性和机密性。最后强调了HTTPS的安全性和可靠性,为现代网络通信提供了坚实的基础。 ... [详细]
author-avatar
Aero-Maxwell
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有