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

使用MultipartFile上传用户头像遇charset=UTF-8不支持错误求助

问题描述

我使用MultipartFile实现用户头像上传功能时,始终收到“charset=UTF-8 is not supported”的错误提示。尝试过多种注解配置(例如在@PostMapping中设置consumes={MediaType.APPLICATION_JSON_UTF8_VALUE}),也查找过相关解决方案,但均无效。怀疑是Spring Security配置导致的问题——因为移除Spring Security的镜像项目中该功能完全正常。用Postman测试时:请求体为form-data会被拒绝,但使用raw/json(不带文件)时请求正常。


User实体类

@Entity
@Table(name="user", uniqueConstraints = {
        @UniqueConstraint(columnNames = "username"),
        @UniqueConstraint(columnNames = "email")
})
public class User {
    @Id
    @Column(name="id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long Id;
    @Column(name="username")
    private String username;
    @Column(name="password")
    private String password;
    @Column(name="avatar")
    private String avatar;
    @Column(name="email")
    private String email;
    @Column(name="bio")
    private String bio;
    @Column(name = "created_at")
    private Date createdAt;
    @Column(name="updated_at")
    private Date updatedAt;
    @ManyToMany(fetch = FetchType.LAZY)
    @JsonIgnore
    @JoinTable(name="user_role",
            joinColumns = @JoinColumn(name="user_id"),
            inverseJoinColumns = @JoinColumn(name="role_id"))
    private Set<Role> roles = new HashSet<>();
    @OneToMany(mappedBy = "donor", cascade = CascadeType.ALL)
    @JsonIgnore
    // liste des donations par donateurs
    private Set<Donation> donationsByDonor = new HashSet<>();
    @OneToMany(mappedBy = "beneficiary", cascade = CascadeType.ALL)
    @JsonIgnore
    // liste des donations par bénéficiaire
    private Set<Donation> donationsByBeneficiary = new HashSet<>();
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    // liste des evaluations par user
    private Set<Evaluation> evaluations = new HashSet<>();
    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
    @JsonIgnore
    // Liste des pdfs par user
    private Set<Pdf> pdfs = new HashSet<>();
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    @JsonIgnore
    // liste des recherches par user
    private Set<Search> searches = new HashSet<>();
    @OneToMany(mappedBy = "alertLauncher", cascade = CascadeType.ALL)
    @JsonIgnore
    // liste alerts par user
    private Set<Alert> alertList = new HashSet<>();

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public User(String username, String email, String password, String avatar) {
        this.username = username;
        this.email = email;
        this.password = password;
        this.avatar = avatar;
    }

    public User(String username, String password, String avatar, String email, String bio, Date createdAt, Date updatedAt, Set<Role> roles, Set<Donation> donationsByBeneficiary, Set<Donation> donationsByDonor, Set<Evaluation> evaluations, Set<Pdf> pdfs, Set<Search> searches, Set<Alert> alertList) {
        this.username = username;
        this.password = password;
        this.avatar = avatar;
        this.email = email;
        this.bio = bio;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
        this.roles = roles;
        this.donationsByBeneficiary = donationsByBeneficiary;
        this.donationsByDonor = donationsByDonor;
        this.evaluations = evaluations;
        this.pdfs = pdfs;
        this.searches = searches;
        this.alertList = alertList;
    }

    public Long getId() {
        return Id;
    }

    public void setId(Long id) {
        Id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getBio() {
        return bio;
    }

    public void setBio(String bio) {
        this.bio = bio;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }

    public Date getUpdatedAt() {
        return updatedAt;
    }

    public void setUpdatedAt(Date updatedAt) {
        this.updatedAt = updatedAt;
    }

    public Set<Role> getRoles() {
        return roles;
    }

    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }

    public Set<Donation> getDonationsByDonor() {
        return donationsByDonor;
    }

    public void setDonationsByDonor(Set<Donation> donationsByDonor) {
        this.donationsByDonor = donationsByDonor;
    }

    public Set<Donation> getDonationsByBeneficiary() {
        return donationsByBeneficiary;
    }

    public void setDonationsByBeneficiary(Set<Donation> donationsByBeneficiary) {
        this.donationsByBeneficiary = donationsByBeneficiary;
    }

    public Set<Evaluation> getEvaluations() {
        return evaluations;
    }

    public void setEvaluations(Set<Evaluation> evaluations) {
        this.evaluations = evaluations;
    }

    public Set<Pdf> getPdfs() {
        return pdfs;
    }

    public void setPdfs(Set<Pdf> pdfs) {
        this.pdfs = pdfs;
    }

    public Set<Search> getSearches() {
        return searches;
    }

    public void setSearches(Set<Search> searches) {
        this.searches = searches;
    }

    public Set<Alert> getAlertList() {
        return alertList;
    }

    public void setAlertList(Set<Alert> alertList) {
        this.alertList = alertList;
    }
}

客户端传入DTO

@Data
public class UserDTOWayIN {
    String username;
    String password;
    String avatar;
    String email;
    String bio;
    Date createdAt;
    Date updatedAt;
    Set<Role> roles;
}

返回客户端DTO

@Data
public class UserDTO {
     Long Id;
     String username;
     String bio;
     Date createdAt;
     Date updatedAt;
     Set<Role> roles;
}

包含注册接口的UserController

@CrossOrigin(origins = "http://localhost:4200", maxAge = 3600)
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService service;
    @Autowired
    private ModelMapper modelMapper;
    public static String uploadDirectory = System.getProperty("user.dir")+"/src/main/webapp/avatars/";

    @PostMapping(path="/new", consumes={MediaType.APPLICATION_JSON_UTF8_VALUE})
    @PreAuthorize("hasRole('ADMIN'), hasRole('USER')")
    public ResponseEntity<UserDTO> saveUser(@ModelAttribute UserDTOWayIN clientDatas, @RequestParam("file")MultipartFile file) throws IOException {
        String originalFilename = file.getOriginalFilename();
        Path fileNameAndPath= Paths.get(uploadDirectory, originalFilename);
        Files.write(fileNameAndPath, file.getBytes());
        clientDatas.setAvatar(originalFilename);
        // Conversion des datas front en DTOWayIN
        UserDTOWayIN userDTOWayIN = modelMapper.map(clientDatas, UserDTOWayIN.class);
        // Conversion sens DTOWayIN à Entité
        User user = service.saveUser(userDTOWayIN);
        // Conversion sens Entité à DTO
        UserDTO userDTO = modelMapper.map(user, UserDTO.class);
        return new ResponseEntity<UserDTO>(userDTO, HttpStatus.CREATED);
    }
}

解决方案

问题根源

  1. 请求类型不匹配:控制器@PostMapping指定了仅接收APPLICATION_JSON_UTF8_VALUE,但form-data请求的Content-Type是multipart/form-data,导致请求被拦截并抛出charset不支持错误。
  2. Spring Security CSRF拦截:默认CSRF防护会拦截form-data请求(未携带CSRF令牌时),而raw/json请求可能通过了其他验证逻辑。
  3. 权限注解语法错误:@PreAuthorize("hasRole('ADMIN'), hasRole('USER')")的EL表达式写法错误,正确的多角色判断应该用hasAnyRole。

修复步骤

1. 修正控制器请求配置

将@PostMapping的consumes改为支持multipart/form-data,或直接删除consumes让Spring自动适配:

@PostMapping(path="/new", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@PreAuthorize("hasAnyRole('ADMIN','USER')")
public ResponseEntity<UserDTO> saveUser(@ModelAttribute UserDTOWayIN clientDatas, @RequestParam("file")MultipartFile file) throws IOException {
    // 原有业务逻辑
}

2. 调整Spring Security配置

针对前后端分离场景,配置CSRF令牌传递方式,并显式支持multipart请求:

Spring Security 5.x版本

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and()
            .authorizeRequests()
            .antMatchers("/user/new").hasAnyRole("ADMIN","USER")
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .multipartConfigElement(multipartConfigElement());
    }

    @Bean
    public MultipartConfigElement multipartConfigElement() {
        MultipartConfigFactory factory = new MultipartConfigFactory();
        factory.setMaxFileSize(DataSize.ofMegabytes(10));
        factory.setMaxRequestSize(DataSize.ofMegabytes(10));
        return factory.createMultipartConfig();
    }
}

Spring Security 6.x版本

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/user/new").hasAnyRole("ADMIN","USER")
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults())
            .multipart(multipart -> multipart
                .maxFileSize(DataSize.ofMegabytes(10))
                .maxRequestSize(DataSize.ofMegabytes(10))
            );
        return http.build();
    }
}

3. Postman测试注意事项

  • 请求类型选择form-data,分别添加UserDTOWayIN的属性字段(如username、password)和名为file的文件字段。
  • 若启用CSRF,需先从Cookie中获取XSRF-TOKEN,并在请求头添加X-XSRF-TOKEN字段,值为获取到的令牌。

内容的提问来源于stack exchange,提问作者JEROME RICHARD

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 02:45:02