Spring Boot 3 + MinIO集群 + Nginx 负载均衡 实现图片(头像)的上传 + 更新替换 + 下载 简单案例

1. 容器准备

1.1 容器结构

1.2 启动容器

1.3 docker-compose.yml

XML 复制代码
version: '3.8'  # 指定 Docker Compose 文件的版本,这里使用版本 3.8

services:
  minio1:
    image: minio/minio:latest  # 使用最新的 MinIO 镜像来创建 MinIO 服务的容器
    volumes:
      - ./data1-1:/data1  # 将当前目录下的 data1-1 文件夹挂载到容器内的 /data1 目录
      - ./data1-2:/data2  # 将当前目录下的 data1-2 文件夹挂载到容器内的 /data2 目录
    ports:
      - "9001:9000"  # 将主机的 9001 端口映射到容器的 9000 端口,这是 MinIO 数据服务端口
      - "9005:9001"  # 将主机的 9005 端口映射到容器的 9001 端口,这是 MinIO 控制台端口
    environment:
      MINIO_ROOT_USER: minioadmin  # 设置 MinIO 的根用户用户名
      MINIO_ROOT_PASSWORD: minioadminpassword  # 设置 MinIO 的根用户密码
    command: server http://minio{1...4}/data{1...2} --console-address ":9001"  # 启动 MinIO 服务并指定控制台端口
    networks:
      - minio-net  # 将 MinIO 服务加入到名为 minio-net 的网络中

  minio2:
    image: minio/minio:latest  # 使用最新的 MinIO 镜像来创建第二个 MinIO 服务的容器
    volumes:
      - ./data2-1:/data1  # 将当前目录下的 data2-1 文件夹挂载到容器内的 /data1 目录
      - ./data2-2:/data2  # 将当前目录下的 data2-2 文件夹挂载到容器内的 /data2 目录
    ports:
      - "9002:9000"  # 将主机的 9002 端口映射到容器的 9000 端口,这是 MinIO 数据服务端口
      - "9006:9001"  # 将主机的 9006 端口映射到容器的 9001 端口,这是 MinIO 控制台端口
    environment:
      MINIO_ROOT_USER: minioadmin  # 设置 MinIO 的根用户用户名
      MINIO_ROOT_PASSWORD: minioadminpassword  # 设置 MinIO 的根用户密码
    command: server http://minio{1...4}/data{1...2} --console-address ":9001"  # 启动 MinIO 服务并指定控制台端口
    networks:
      - minio-net  # 将 MinIO 服务加入到名为 minio-net 的网络中

  minio3:
    image: minio/minio:latest  # 使用最新的 MinIO 镜像来创建第三个 MinIO 服务的容器
    volumes:
      - ./data3-1:/data1  # 将当前目录下的 data3-1 文件夹挂载到容器内的 /data1 目录
      - ./data3-2:/data2  # 将当前目录下的 data3-2 文件夹挂载到容器内的 /data2 目录
    ports:
      - "9003:9000"  # 将主机的 9003 端口映射到容器的 9000 端口,这是 MinIO 数据服务端口
      - "9007:9001"  # 将主机的 9007 端口映射到容器的 9001 端口,这是 MinIO 控制台端口
    environment:
      MINIO_ROOT_USER: minioadmin  # 设置 MinIO 的根用户用户名
      MINIO_ROOT_PASSWORD: minioadminpassword  # 设置 MinIO 的根用户密码
    command: server http://minio{1...4}/data{1...2} --console-address ":9001"  # 启动 MinIO 服务并指定控制台端口
    networks:
      - minio-net  # 将 MinIO 服务加入到名为 minio-net 的网络中

  minio4:
    image: minio/minio:latest  # 使用最新的 MinIO 镜像来创建第四个 MinIO 服务的容器
    volumes:
      - ./data4-1:/data1  # 将当前目录下的 data4-1 文件夹挂载到容器内的 /data1 目录
      - ./data4-2:/data2  # 将当前目录下的 data4-2 文件夹挂载到容器内的 /data2 目录
    ports:
      - "9004:9000"  # 将主机的 9004 端口映射到容器的 9000 端口,这是 MinIO 数据服务端口
      - "9008:9001"  # 将主机的 9008 端口映射到容器的 9001 端口,这是 MinIO 控制台端口
    environment:
      MINIO_ROOT_USER: minioadmin  # 设置 MinIO 的根用户用户名
      MINIO_ROOT_PASSWORD: minioadminpassword  # 设置 MinIO 的根用户密码
    command: server http://minio{1...4}/data{1...2} --console-address ":9001"  # 启动 MinIO 服务并指定控制台端口
    networks:
      - minio-net  # 将 MinIO 服务加入到名为 minio-net 的网络中

  nginx:
    image: nginx:latest  # 使用最新的 Nginx 镜像来创建 Nginx 服务的容器
    container_name: nginx  # 为容器命名为 nginx
    ports:
      - "80:80"  # 将主机的 80 端口映射到容器的 80 端口,这是 HTTP 服务端口
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro  # 将本地的 nginx 配置文件挂载到容器内的 nginx 配置目录,设置为只读
    networks:
      - minio-net  # 将 Nginx 服务加入到名为 minio-net 的网络中

volumes:
  data1-1:
  data1-2:
  data2-1:
  data2-2:
  data3-1:
  data3-2:
  data4-1:
  data4-2:

networks:
  minio-net:  # 定义一个名为 minio-net 的网络,用于连接 MinIO 和 Nginx 服务

1.4 nginx.conf

XML 复制代码
# 设置 Nginx 进程数,这里设置为 1。
worker_processes 1;

# 配置 Nginx 的事件模块,处理基本的事件逻辑。
events {
    # 设置每个工作进程的最大连接数。
    worker_connections 1024;
}

# 配置 Nginx 的 HTTP 模块。
http {
    # 定义一个上游服务器组,名为 minio,包含四个 MinIO 实例。
    upstream minio {
        # 添加 MinIO 实例到上游服务器组,所有请求将被均衡地分配到这些实例上。
        server minio1:9000;
        server minio2:9000;
        server minio3:9000;
        server minio4:9000;
    }

    # 定义一个服务器块,处理来自客户端的 HTTP 请求。
    server {
        # 监听 80 端口,这里是 HTTP 默认端口。
        listen 80;

        # 定义一个位置块,处理所有的 URL 路径(即 '/' 开始的路径)。
        location / {
            # 将所有请求转发到上游服务器组 minio。
            proxy_pass http://minio;

            # 设置主机头信息为客户端请求中的 Host 头信息。
            proxy_set_header Host $host;

            # 设置客户端的真实 IP 地址。
            proxy_set_header X-Real-IP $remote_addr;

            # 设置客户端请求经过的代理服务器地址。
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            # 设置客户端请求的协议(HTTP 或 HTTPS)。
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

1.5 运行结果

1.5.1 自动创建目录

1.5.2 访问MinIO

说明:

http://192.168.186.77:9005/login

http://192.168.186.77:9006/login

http://192.168.186.77:9007/login

http://192.168.186.77:9008/login

访问4个不同的端口代表不同的MinIO,我是在Ubuntu24.04 TLS版安装的,你可以通过ifconfig命令查看自己本机的IP,也就是NGINX对外的IP,自行替换192.168.186.77。

1.5.3 登录MinIO

说明:用户名和密码是你自己在docker-compose.yml里面配置的用户名和密码。

1.5.4 创建Buckets

桶命名规则:桶名称必须在3到63个字符之间,只能使用小写字母、数字、连字符(-)和点(.),名称不能以点(.)或者连字符(-)开始或结束,桶名称不能是IP地址格式,在一个MinIO实例中,桶名称必须是唯一的,不同用户在同一个MinIO实例中也不能使用同名桶。。

1.5.5 开放访问权限

操作:在创建的桶旁边点击Manage,把Access Policy修改为Public,你可以自行查找更安全的方式进行访问。

2. 项目结构

3. pom.xml

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.example</groupId>
    <artifactId>minio</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- https://mvnrepository.com/artifact/io.minio/minio -->
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.5.11</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

说明:通过Maven引入MinIO 和 Spring Boot 以及数据库相关的依赖。

4. application.yml

XML 复制代码
spring:
  application:
    name: spring_minio
  datasource:
    url: jdbc:mysql://localhost:3306/minio
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    open-in-view: false
minio:
  url: http://192.168.186.77   # 使用nginx的地址,因为它会代理到MinIO集群
  access-key: minioadmin
  secret-key: minioadminpassword
  bucket: bucket
  base-url: http://192.168.186.77/bucket/ # 代理后的base url

说明:每个 MinIO 实例的实际数据服务端口是 9000(在 Docker Compose 中映射为 9001, 9002, 9003, 9004)。你的 Spring Boot 应用程序通过 Nginx 代理访问 MinIO 集群,因此配置中的 URL 和 base-url 应该指向 Nginx。

5. SpringMinioApplication.java

XML 复制代码
package org.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringMinioApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringMinioApplication.class, args);
    }
}

6. User.java

XML 复制代码
package org.example.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;

@Data
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String email;
    private String password;
    private String imageUrl;
}

7. UserRepository.java

XML 复制代码
package org.example.repository;

import org.example.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}

8. UserService.java

XML 复制代码
package org.example.service;

import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.GetObjectArgs;
import io.minio.RemoveObjectArgs;
import org.example.entity.User;
import org.example.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.util.UUID;
import java.io.InputStream;

@Service
public class UserService {

    @Autowired
    private MinioClient minioClient;

    @Autowired
    private UserRepository userRepository;

    @Value("${minio.bucket}")
    private String bucketName;

    @Value("${minio.base-url}")
    private String baseUrl;

    public User updateUserAvatar(Long userId, MultipartFile file) {
        User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("User not found"));
        String originalFileName = file.getOriginalFilename();
        String extension = null;
        if (originalFileName != null) {
            extension = originalFileName.substring(originalFileName.lastIndexOf('.'));
        }
        String fileName = UUID.randomUUID() + extension;

        try {
            // 上传新的头像
            minioClient.putObject(
                    PutObjectArgs.builder().bucket(bucketName).object(fileName).stream(
                                    file.getInputStream(), file.getSize(), -1)
                            .contentType(file.getContentType())
                            .build());

            // 保存图片文件名到数据库
            String oldFileName = user.getImageUrl();
            user.setImageUrl(fileName);
            userRepository.save(user);

            // 删除旧的头像
            if (oldFileName != null && !oldFileName.isEmpty()) {
                minioClient.removeObject(
                        RemoveObjectArgs.builder().bucket(bucketName).object(oldFileName).build());
            }

            // 返回对象信息 拼接的图片路径 + 空密码
            String imageUrl = baseUrl + fileName;
            user.setImageUrl(imageUrl);
            user.setPassword(null);
            return user;
        } catch (Exception e) {
            // 如果上传新头像失败,抛出异常
            throw new RuntimeException("Error updating user avatar", e);
        }
    }

    public InputStream downloadFile(String fileName) throws Exception {
        return minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .build());
    }

    public void register(User user) {
        User existingUser = userRepository.findByUsername(user.getUsername());
        if (existingUser != null) {
            throw new IllegalStateException("Username already exists.");
        }
        userRepository.save(user);
    }

    public User login(User user) {
        User foundUser = userRepository.findByUsername(user.getUsername());
        if (foundUser != null && foundUser.getPassword().equals(user.getPassword())) {
            foundUser.setPassword(null);
            foundUser.setImageUrl(baseUrl + foundUser.getImageUrl());
            return foundUser;
        }
        return null;
    }
}

说明:只做简单的登录,注册操作并没有进行严格的认证,比如token认证或有权访问,只是模拟图片的上传和下载,通过**MultipartFile** 接收上传的文件。使用**UUID** 生成唯一的文件名,以避免文件名冲突。使用**MinioClient.putObject** 方法将文件上传到MinIO桶中。将新上传的文件名保存到用户数据库中。删除旧的头像文件,以节省存储空间。使用**MinioClient.getObject**方法从MinIO桶中下载指定文件。

9. UserController.java

XML 复制代码
package org.example.controer;
import org.example.entity.User;
import org.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;

@Controller
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/login")
    @ResponseBody
    public ResponseEntity<User> login(@RequestBody User user) {
            User pass = userService.login(user);
            if (pass!=null) {
                return ResponseEntity.ok(pass);
            }
            return ResponseEntity.status(400).body(null);
    }

    @PostMapping("/register")
    @ResponseBody
    public ResponseEntity<String> registerUser(@RequestBody User user) {
        try {
            userService.register(user);
            return ResponseEntity.ok("注册成功! ");
        } catch (IllegalStateException e) {
            return ResponseEntity.status(400).body(e.getMessage());
        } catch (Exception e) {
            return ResponseEntity.status(500).body("注册失败! ");
        }
    }

    @PostMapping("/upload/{id}")
    @ResponseBody
    public ResponseEntity<User> uploadUserAvatar(@PathVariable Long id, @RequestParam("file") MultipartFile file) {
        try {
            return ResponseEntity.ok(userService.updateUserAvatar(id, file));
        } catch (Exception e) {
            return ResponseEntity.status(500).body(null);
        }
    }
    @GetMapping("/download/{fileName}")
    @ResponseBody
    public ResponseEntity<InputStreamResource> downloadFile(@PathVariable String fileName) {
        try {
            InputStream inputStream = userService.downloadFile(fileName);
            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
                    .body(new InputStreamResource(inputStream));
        } catch (Exception e) {
            return ResponseEntity.status(500).body(null);
        }
    }
}

10. MinioConfig.java

XML 复制代码
package org.example.config;

import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MinioConfig {

    @Value("${minio.url}")
    private String minioUrl;

    @Value("${minio.access-key}")
    private String accessKey;

    @Value("${minio.secret-key}")
    private String secretKey;

    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(minioUrl)
                .credentials(accessKey, secretKey)
                .build();
    }
}

11. index.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>用户管理</title>
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.1.3/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
    <style>
        body {
            background-color: #f5f5f5;
            font-family: 'Arial', sans-serif;
        }
        .container {
            max-width: 600px;
            margin-top: 50px;
        }
        .card {
            border-radius: 15px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
            overflow: hidden;
        }
        .card-header {
            background-color: #007bff;
            color: white;
            text-align: center;
            font-size: 1.5rem;
            padding: 1rem;
        }
        .nav-tabs {
            border-bottom: none;
        }
        .nav-tabs .nav-link {
            border: none;
            color: #007bff;
        }
        .nav-tabs .nav-link.active {
            color: white;
            background-color: #007bff;
            border-radius: 0;
        }
        .form-control {
            border-radius: 50px;
            padding-left: 2.5rem;
        }
        .form-icon {
            position: absolute;
            left: 15px;
            top: 50%;
            transform: translateY(-50%);
            color: #007bff;
        }
        .btn-primary {
            border-radius: 50px;
            background-color: #007bff;
            border: none;
        }
        .btn-primary:hover {
            background-color: #0056b3;
        }
        .avatar {
            width: 150px;
            height: 150px;
            border-radius: 50%;
            margin: 20px auto;
            display: block;
            object-fit: cover;
        }
        .text-center {
            text-align: center;
        }
        .mt-4 {
            margin-top: 1.5rem;
        }
        .position-relative {
            position: relative;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="card" id="auth-card">
        <div class="card-header">
            用户管理
        </div>
        <div class="card-body">
            <ul class="nav nav-tabs justify-content-center" id="authTabs" role="tablist">
                <li class="nav-item" role="presentation">
                    <button class="nav-link active" id="login-tab" data-bs-toggle="tab" data-bs-target="#login" type="button" role="tab" aria-controls="login" aria-selected="true">登录</button>
                </li>
                <li class="nav-item" role="presentation">
                    <button class="nav-link" id="register-tab" data-bs-toggle="tab" data-bs-target="#register" type="button" role="tab" aria-controls="register" aria-selected="false">注册</button>
                </li>
            </ul>
            <div class="tab-content mt-4" id="authTabsContent">
                <div class="tab-pane fade show active" id="login" role="tabpanel" aria-labelledby="login-tab">
                    <form id="loginForm">
                        <div class="mb-3 position-relative">
                            <i class="fas fa-user form-icon"></i>
                            <input type="text" class="form-control" id="loginUsername" placeholder="用户名" required>
                        </div>
                        <div class="mb-3 position-relative">
                            <i class="fas fa-lock form-icon"></i>
                            <input type="password" class="form-control" id="loginPassword" placeholder="密码" required>
                        </div>
                        <button type="button" class="btn btn-primary w-100" onclick="login()">登录</button>
                    </form>
                </div>
                <div class="tab-pane fade" id="register" role="tabpanel" aria-labelledby="register-tab">
                    <form id="registerForm">
                        <div class="mb-3 position-relative">
                            <i class="fas fa-user form-icon"></i>
                            <input type="text" class="form-control" id="registerUsername" placeholder="用户名" required>
                        </div>
                        <div class="mb-3 position-relative">
                            <i class="fas fa-lock form-icon"></i>
                            <input type="password" class="form-control" id="registerPassword" placeholder="密码" required>
                        </div>
                        <div class="mb-3 position-relative">
                            <i class="fas fa-envelope form-icon"></i>
                            <input type="email" class="form-control" id="registerEmail" placeholder="电子邮箱" required>
                        </div>
                        <button type="button" class="btn btn-primary w-100" onclick="register()">注册</button>
                    </form>
                </div>
            </div>
        </div>
    </div>

    <div class="card d-none mt-4" id="user-card">
        <div class="card-header">
            用户信息
        </div>
        <div class="card-body text-center">
            <img id="userAvatar" class="avatar" src="" alt="用户头像">
            <p><strong>用户名:</strong> <span id="displayUsername"></span></p>
            <p><strong>电子邮箱:</strong> <span id="displayEmail"></span></p>
            <form id="uploadForm">
                <div class="mb-3">
                    <input type="file" class="form-control" id="file" required>
                </div>
                <button type="button" class="btn btn-primary w-100" onclick="upload()">上传头像</button>
            </form>
            <a id="downloadLink" href="#" class="btn btn-secondary w-100 mt-3" download>下载头像</a>
        </div>
    </div>
</div>

<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
<script>
    function login() {
        const username = document.getElementById('loginUsername').value;
        const password = document.getElementById('loginPassword').value;

        axios.post('/login', { username, password })
            .then(response => {
                alert('登录成功');
                // 保存用户信息到localStorage
                localStorage.setItem('username', username);
                localStorage.setItem('email', response.data.email);
                localStorage.setItem('userId', response.data.id);
                localStorage.setItem('imageUrl', response.data.imageUrl);
                // 显示用户信息
                displayUserInfo();
            })
            .catch(error => {
                alert('登录失败: ' + error.response.data);
            });
    }

    function register() {
        const username = document.getElementById('registerUsername').value;
        const password = document.getElementById('registerPassword').value;
        const email = document.getElementById('registerEmail').value;

        axios.post('/register', { username, password, email })
            .then(response => {
                alert('注册成功');
            })
            .catch(error => {
                alert('注册失败: ' + error.response.data);
            });
    }

    function upload() {
        const userId = localStorage.getItem('userId');
        const file = document.getElementById('file').files[0];

        const formData = new FormData();
        formData.append('file', file);

        axios.post(`/upload/${userId}`, formData, {
            headers: {
                'Content-Type': 'multipart/form-data'
            }
        })
            .then(response => {
                alert('上传成功');
                localStorage.setItem('imageUrl', response.data.imageUrl);
                displayUserInfo();
            })
            .catch(error => {
                alert('上传失败: ' + error.response.data);
            });
    }

    function displayUserInfo() {
        const username = localStorage.getItem('username');
        const email = localStorage.getItem('email');
        const imageUrl = localStorage.getItem('imageUrl');

        if (username && email && imageUrl) {
            document.getElementById('displayUsername').innerText = username;
            document.getElementById('displayEmail').innerText = email;
            document.getElementById('userAvatar').src = imageUrl;
            document.getElementById('auth-card').classList.add('d-none');
            document.getElementById('user-card').classList.remove('d-none');
            let filename = imageUrl.substring(imageUrl.lastIndexOf('/') + 1)
            document.getElementById('downloadLink').href = `/download/${filename}`;
        }
    }

    // 登录后显示用户信息
    window.onload = displayUserInfo;
</script>
</body>
</html>

12. 测试验证

12.1.1 注册账号

说明:先注册一个账号,刚开始图片是NULL的。

12.1.2 登录账号

说明:进行登录,因为之前上传过图片,所以有图片,如果你想退出登录调出控制台清除下面字段刷新即可返回登录界面。

12.1.3 文件上传测试

说明:先选择一张图片,点击上传头像,我上传了一张图片,图片进行回显。

12.1.4 文件下载测试

说明:点击下载头像,头像进行下载。

12.1.5 MinIO控制台预览

12.1.6 数据库数据

13. 总结

通过docker-compose实现了MinIO集群和Nginx的负载均衡,通过Spring Boot 整合MinIo实现图片的上传和下载。

相关推荐
2402_857583491 分钟前
“协同过滤技术实战”:网上书城系统的设计与实现
java·开发语言·vue.js·科技·mfc
白宇横流学长2 分钟前
基于SpringBoot的停车场管理系统设计与实现【源码+文档+部署讲解】
java·spring boot·后端
APP 肖提莫5 分钟前
MyBatis-Plus分页拦截器,源码的重构(重构total总数的计算逻辑)
java·前端·算法
kirito学长-Java7 分钟前
springboot/ssm太原学院商铺管理系统Java代码编写web在线购物商城
java·spring boot·后端
爱学习的白杨树8 分钟前
MyBatis的一级、二级缓存
java·开发语言·spring
MrJson-架构师8 分钟前
4.银河麒麟V10(ARM) 离线安装 MySQL
arm开发·mysql
Code成立18 分钟前
《Java核心技术I》Swing的网格包布局
java·开发语言·swing
中草药z24 分钟前
【Spring】深入解析 Spring 原理:Bean 的多方面剖析(源码阅读)
java·数据库·spring boot·spring·bean·源码阅读
信徒_32 分钟前
常用设计模式
java·单例模式·设计模式
神仙别闹37 分钟前
基于C#实现的(WinForm)模拟操作系统文件管理系统
java·git·ffmpeg