Integrating Spring Boot with Minio for Direct File Upload Using Presigned Credentials
This article explains how to integrate Spring Boot with Minio to enable direct client‑side file uploads by generating time‑limited presigned credentials on the backend, covering environment setup, Maven dependencies, configuration classes, REST endpoints, response fields, and complete front‑end examples including chunked and resumable uploads.
Spring Boot can be integrated with Minio to provide two file‑upload approaches: uploading through the backend (which allows centralized authentication, permission control, and additional processing) and uploading directly to Minio using a presigned credential generated by the backend.
Environment Preparation
A running Minio instance is required, e.g., http://mylocalhost:9001 .
Spring Boot Integration with Minio
1. Add Minio dependency
# pom.xml
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>7.1.0</version>
</dependency>2. Configuration (application.yml)
# application.yml
minio:
endpoint: http://mylocalhost:9001
accessKey: minio
secretKey: minio123
bucket: demo3. Property class
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucket;
// getters and setters ...
}4. Minio configuration bean
@Configuration
public class MinioConfig {
@Bean
public MinioClient minioClient(MinioProperties properties) {
try {
MinioClient.Builder builder = MinioClient.builder();
builder.endpoint(properties.getEndpoint());
if (StringUtils.hasLength(properties.getAccessKey()) && StringUtils.hasLength(properties.getSecretKey())) {
builder.credentials(properties.getAccessKey(), properties.getSecretKey());
}
return builder.build();
} catch (Exception e) {
return null;
}
}
}Presigned Upload Credential Endpoint
A POST endpoint /presign returns a map containing the fields required for a direct Minio upload.
@RequestMapping(value = "/presign", method = {RequestMethod.POST})
public Map
presign(@RequestBody PresignParam presignParam) {
if (StringUtils.isEmpty(presignParam.getBucket())) {
presignParam.setBucket("demo");
}
if (StringUtils.isEmpty(presignParam.getFilename())) {
presignParam.setFilename(UUID.randomUUID().toString());
}
ZonedDateTime expirationDate = ZonedDateTime.now().plusMinutes(10);
PostPolicy policy = new PostPolicy(presignParam.getBucket(), presignParam.getFilename(), expirationDate);
try {
Map
map = minioClient.presignedPostPolicy(policy);
return map;
} catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}The response includes fields such as bucket , x-amz-date , x-amz-signature , key , x-amz-algorithm , x-amz-credential , and policy .
Request Parameter Class
public class PresignParam {
private String bucket; // bucket name
private String filename; // object name
// getters and setters ...
}Front‑End Upload Using the Credential
After obtaining the credential, the front‑end builds a FormData object and posts it directly to Minio.
uploadFile(file, policy) {
console.log("准备上传文件:");
console.log("file:" + file);
console.log("policy:" + policy);
var formData = new FormData();
formData.append('file', file);
formData.append('key', policy['key']);
formData.append('x-amz-algorithm', policy['x-amz-algorithm']);
formData.append('x-amz-credential', policy['x-amz-credential']);
formData.append('x-amz-signature', policy['x-amz-signature']);
formData.append('x-amz-date', policy['x-amz-date']);
formData.append('policy', policy['policy']);
return new Promise((resolve, reject) => {
$.ajax({
method: 'POST',
url: 'http://mylocalhost:9001/' + policy['bucket'],
data: formData,
dataType: 'json',
contentType: false,
processData: false,
xhr: function () {
var xhr = $.ajaxSettings.xhr();
if (xhr.upload) {
xhr.upload.addEventListener('progress', function (e) {
var percentage = parseInt(e.loaded / e.total * 100);
vm.uploadResult = percentage + "%" + ":" + policy['key'];
}, false);
}
return xhr;
},
success: function (result) {
vm.uploadResult = '文件上传成功:' + policy['key'];
resolve(result);
},
error: function (e) {
reject();
}
});
});
}Chunked, Instant and Resumable Uploads
For large files, the article demonstrates splitting a file into 5 MB chunks, calculating an MD5 for each chunk (using SparkMD5), uploading each chunk with its own presigned credential, and finally merging the chunks on the server.
Chunked Upload Example (Vue.js)
sliceEvent() {
var file = this.file;
var chunkSize = 5 * 1024 * 1024; // 5 MB
var totalChunk = Math.ceil(file.size / chunkSize);
var chunks = [];
for (var i = 0; i < totalChunk; i++) {
var start = i * chunkSize;
var end = Math.min(file.size, start + chunkSize);
var blob = file.slice(start, end);
chunks.push(blob);
}
this.sliceUploadResult = Array(totalChunk).fill(0);
var index = 0;
while (index < totalChunk) {
var params = { bucket: "slice", filename: index + "寂寞的季节.mp4" };
var policyPromise = this.requestPolicy(params);
((idx) => {
var fileChunk = chunks[idx];
policyPromise.then((result) => {
var filename = result['key'];
vm.uploadFile(fileChunk, result).then(() => {
vm.sliceUploadResult[idx] = '分片文件上传成功:' + filename;
});
});
})(index);
index++;
}
}Merge Chunks
@GetMapping("/compose")
public void merge() {
List
sources = new ArrayList<>();
sources.add(ComposeSource.builder().bucket("slice").object("0寂寞的季节.mp4").build());
sources.add(ComposeSource.builder().bucket("slice").object("1寂寞的季节.mp4").build());
sources.add(ComposeSource.builder().bucket("slice").object("2寂寞的季节.mp4").build());
ComposeObjectArgs args = ComposeObjectArgs.builder()
.bucket("demo")
.object("寂寞的季节.mp4")
.sources(sources)
.build();
try {
minioClient.composeObject(args);
} catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
}Complete Front‑End Example (HTML + Vue.js + jQuery)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Minio Test</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.14/vue.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body>
<div id="app">
<h1>{{title}}</h1>
<form @submit.prevent="getPolicyForm">
<label>桶名<input type="text" v-model="policyParams.bucket"></label><br>
<label>文件名<input type="text" v-model="policyParams.filename"></label><br>
<button type="submit">获取上传凭证</button>
</form>
<form @submit.prevent="uploadFileForm" v-show="policy != null">
<label>文件<input type="file" @change="fileChange"></label><br>
<button type="submit" v-show="file != null">上传文件</button>
</form>
<div v-show="file != null">
<button @click="sliceEvent">测试文件分片上传</button>
<button @click="sliceComposeEvent">分片文件合并</button>
</div>
<p>{{uploadResult}}</p>
<ul>
<li v-for="(item, index) in sliceUploadResult" :key="index">{{item}}</li>
</ul>
</div>
<script>
var vm = new Vue({
el: "#app",
data() {
return {
title: "Minio测试",
policyParams: { bucket: null, filename: null },
policy: null,
file: null,
uploadResult: null,
sliceUploadResult: null,
sliceCount: 0
};
},
methods: {
getPolicyForm() {
this.policyParams.bucket = "demo";
this.policyParams.filename = "寂寞的季节.mp4";
this.requestPolicy(this.policyParams);
},
requestPolicy(params) {
return new Promise((resolve, reject) => {
$.ajax({
type: "POST",
url: "http://localhost:8888/presign",
contentType: "application/json",
data: JSON.stringify(params),
success: (result) => { vm.policy = result; resolve(result); },
error: () => reject()
});
});
},
fileChange(event) { this.file = event.target.files[0]; },
uploadFileForm() { this.uploadFile(this.file, this.policy); },
// uploadFile implementation as shown earlier …
sliceEvent() { /* chunked upload implementation as shown earlier */ },
sliceComposeEvent() { /* merge request implementation */ }
}
});
</script>
</body>
</html>The article concludes with a reminder that the content is intended for technical discussion and not for advertising.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.