使用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); } }
解决方案
问题根源
- 请求类型不匹配:控制器@PostMapping指定了仅接收
APPLICATION_JSON_UTF8_VALUE,但form-data请求的Content-Type是multipart/form-data,导致请求被拦截并抛出charset不支持错误。 - Spring Security CSRF拦截:默认CSRF防护会拦截form-data请求(未携带CSRF令牌时),而raw/json请求可能通过了其他验证逻辑。
- 权限注解语法错误:
@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
相关产品推荐
相关产品推荐

