You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SpringBoot应用Docker卷配置问题:图片上传路径映射异常

问题描述

我开发了一个允许用户上传图片到服务器的SpringBoot应用,本地非Docker环境运行正常,无需认证即可通过浏览器链接打开图片。但部署到Docker后,用户上传的图片仅存储在容器内的masterclass-photos文件夹中,预期存放图片的宿主机/var/lib/public/images文件夹为空。

如何配置Docker,使其实现与本地运行时相同的图片访问效果?


项目结构

masterclass-photos和motherforum-data文件夹由Docker创建。
项目结构

日志信息

2022-11-23 12:18:31 java.net.UnknownHostException: masterclass-photos
2022-11-23 12:18:31     at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:567) ~[na:na]
2022-11-23 12:18:31     at java.base/java.net.Socket.connect(Socket.java:633) ~[na:na]
2022-11-23 12:18:31     at java.base/sun.net.ftp.impl.FtpClient.doConnect(FtpClient.java:1045) ~[na:na]
2022-11-23 12:18:31     at java.base/sun.net.ftp.impl.FtpClient.tryConnect(FtpClient.java:1010) ~[na:na]
2022-11-23 12:18:31     at java.base/sun.net.ftp.impl.FtpClient.connect(FtpClient.java:1102) ~[na:na]
2022-11-23 12:18:31     at java.base/sun.net.ftp.impl.FtpClient.connect(FtpClient.java:1088) ~[na:na]
...
...
大量堆栈跟踪信息
ru.rcitsakha.motherforum.filter.CustomAuthorizationFilter.doFilterInternal(CustomAuthorizationFilter.java:73) ~[classes!/:1.0-SNAPSHOT]
...
...
大量堆栈跟踪信息

相关代码与配置

CustomAuthorizationFilter类

package ru.rcitsakha.motherforum.filter;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import static java.util.Arrays.stream;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

@Slf4j
public class CustomAuthorizationFilter extends OncePerRequestFilter {
    private String jwtSecret;

    public void setJwtSecret(String jwtSecret) {
        this.jwtSecret = jwtSecret;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        if (request.getServletPath().equals("/api/login") || request.getServletPath().equals("/api/token/refresh/**")) {
            filterChain.doFilter(request, response);
        } else {
            String authorizationHeader = request.getHeader(AUTHORIZATION);
            if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
                try {
                    String token = authorizationHeader.substring("Bearer ".length());
                    Algorithm algorithm = Algorithm.HMAC256(jwtSecret.getBytes()); // 需与authenticationFilter一致
                    JWTVerifier verifier = JWT.require(algorithm).build();
                    DecodedJWT decodedJWT = verifier.verify(token);
                    String username = decodedJWT.getSubject();
                    String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
                    Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
                    stream(roles).forEach(role -> {
                        authorities.add(new SimpleGrantedAuthority(role));
                    });
                    UsernamePasswordAuthenticationToken authenticationToken =
                            new UsernamePasswordAuthenticationToken(username, null, authorities);
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                    filterChain.doFilter(request, response);
                } catch (Exception exception) {
                    log.error("登录错误: {}", exception.getMessage());
                    response.setHeader("error", exception.getMessage());
                    response.setStatus(FORBIDDEN.value());

                    Map<String, String> error = new HashMap<>();
                    error.put("error_message", exception.getMessage());
                    response.setContentType(APPLICATION_JSON_VALUE);
                    new ObjectMapper().writeValue(response.getOutputStream(), error);
                }
            } else {
                filterChain.doFilter(request, response); // 日志中的第73行
            }
        }
    }
}

docker-compose.yml配置

services:
  postgres:
    image: 'postgres:15'
    container_name: 'java-postgres'
    env_file:
      - config/.envfile
    volumes:
      - ./motherforum-data:/var/lib/postgresql/data


  app:
    build: ./
    container_name: 'java-app'
    environment:
      - 'PORT=8080'
      - 'SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/motherforumdb'
    ports:
      - 8080:8080
    depends_on:
      - postgres
    volumes:
      - ./masterclass-photos:/var/lib/public/images

Dockerfile配置

FROM openjdk:17
ADD /target/mother-forum-1.0-SNAPSHOT.jar backend.jar
ENTRYPOINT ["java", "-jar", "/backend.jar"]

资源处理器配置

package ru.rcitsakha.motherforum.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.nio.file.Path;
import java.nio.file.Paths;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        exposeDirectory("masterclass-photos", registry);
    }

    private void exposeDirectory(String dirName, ResourceHandlerRegistry registry) {
        Path uploadDir = Paths.get(dirName);
        String uploadPath = uploadDir.toFile().getAbsolutePath();

        if (dirName.startsWith("../")) {
            dirName = dirName.replace("../", "");
        }

        registry.addResourceHandler("/" + dirName + "/**")
                .addResourceLocations("file:/" + uploadPath + "/");
    }
}

Postman请求情况

该端点无需JWT认证,安全配置为http.authorizeHttpRequests().antMatchers("/api/login/**", "/token/refresh", "api/motherforum/**").permitAll();
Postman请求

文件系统情况

  • 宿主机文件系统中未创建masterclass-photos/3文件夹,但该文件夹在容器内已创建。
    容器内文件夹
  • 预期存放图片的/var/lib/public/images文件夹为空:
    宿主机空文件夹

Java应用容器挂载信息

"Mounts": [
            {
                "Type": "bind",
                "Source": "C:\\ProdApps\\mother-forum\\masterclass-photos",
                "Destination": "/var/lib/public/images",
                "Mode": "rw",
                "RW": true,
                "Propagation": "rprivate"
            }
        ] 

MasterClass类(图片路径相关)

是否需要修改getPhotosImagePath()方法?

package ru.rcitsakha.motherforum.model;


import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Entity
@Table(name = "masterclass")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public class MasterClass {

    private static final String DATE_TIME_FORMATTER = "dd.MM.yyyy HH:mm";

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String title;

    @Column(length = 2000)
    private String description;

    private String address;

    private String dateTime;

    private boolean isExpired;

    @ManyToMany(fetch = FetchType.LAZY,
            cascade = {
                    CascadeType.PERSIST,
                    CascadeType.MERGE
            })
    @JoinTable(
            name = "masterclass_mother",
            joinColumns = @JoinColumn(name = "masterclass_id"),
            inverseJoinColumns = @JoinColumn(name = "mother_id")
    )
    @ToString.Exclude
    @JsonIgnore
    private Set<Mother> mothers = new HashSet<>();

    public void addMother(Mother mother) {
        this.mothers.add(mother);
        mother.getMasterClasses().add(this);
    }

    @Column(length = 128)
    @ElementCollection
    private List<String> photos = new ArrayList<>();

    public void removeMother(long motherId) {
        Mother mother = this.mothers.stream()
                .filter(m -> m.getId() == motherId)
                .findFirst()
                .orElse(null);

        if (mother != null) {
            this.mothers.remove(mother);
            mother.getMasterClasses().remove(this);
        }
    }

    public void copy(MasterClass masterClass) {
        this.title = masterClass.title;
        this.description = masterClass.description;
        this.address = masterClass.address;
        this.dateTime = masterClass.dateTime;
        this.isExpired = masterClass.isExpired;
    }

    @Transient
    public List<String> getPhotosImagePath() {
        if (photos == null) {
            return null;
        }
        return photos.stream()
                .map(photoFileName -> "/masterclass-photos/" + id + "/" + photoFileName)
                .collect(Collectors.toList());
    }
}

解决方案

核心问题分析

当前Docker挂载的是宿主机./masterclass-photos到容器/var/lib/public/images,但应用代码中图片上传、资源映射均基于容器内的masterclass-photos目录,两者路径不匹配,导致文件写入容器内的masterclass-photos而非挂载的宿主机目录。

步骤1:修正Docker挂载路径

修改docker-compose.yml中app服务的volumes配置,将宿主机目录挂载到容器内应用实际使用的路径:

volumes:
  - ./masterclass-photos:/masterclass-photos

这样容器内写入/masterclass-photos的文件会同步到宿主机的./masterclass-photos目录。

步骤2:验证资源映射配置

MvcConfig中已正确映射/masterclass-photos/**到容器内的masterclass-photos目录,无需修改。

步骤3:确认图片上传逻辑的存储路径

检查上传代码,确保文件写入路径为容器内的/masterclass-photos(相对路径或绝对路径均可),避免写入其他未挂载的目录。

步骤4:解决UnknownHostException问题

日志中的java.net.UnknownHostException: masterclass-photos是因为代码错误地将目录名当作主机名访问,检查上传逻辑中是否存在FTP或网络请求操作,替换为本地文件系统读写即可。

步骤5:开放图片访问权限

在Spring Security配置中添加图片路径的匿名访问权限:

http.authorizeHttpRequests().antMatchers("/masterclass-photos/**").permitAll();

确保浏览器可直接访问图片链接,无需认证。


内容的提问来源于stack exchange,提问作者Zakhar Borisov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 13:01:02