Angular调用Spring Boot PUT接口返回403 Postman正常问题求解
Angular 调用 Spring Boot PUT 接口返回403问题修复
问题现象
Angular 12 前端向 Spring Boot 后端用户更新接口发送 PUT 请求时返回 403 Forbidden 响应,参数完全一致的请求通过 Postman 发送可正常完成用户信息更新。
现有代码与日志
前端请求方法
updateUser(userId: string, profilePic: string, shortDes: string, username: string, email: string, phoneNumber: string): Observable<any> { const body: FormData = new FormData(); body.append('id', userId); body.append('username', username); body.append('email', email); body.append('shortDes', shortDes); body.append('profilePic', profilePic); body.append('phoneNum', phoneNumber); console.log(userId,username,) return this.http.put<HttpResponse<any>>(environment.baseUrl + `/api/v1/users/` + userId, body,{ observe: 'response' }); }
后端接收方法
@PutMapping( value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE ) @ResponseStatus(HttpStatus.OK) @ResponseBody public ResponseEntity<?> updateUser( @ModelAttribute UserBody body, @PathVariable("id") int id){ if (body.getId() != id) { return new ResponseEntity<>("Mismatch userId !!", HttpStatus.BAD_REQUEST); } else if (true) { // TODO: 5/26/2022 handle the multi part file size from this stage before save it } try { String filePath = null; if (body.getProfilePic() != null) { filePath = fileService.saveUserProfilePicture(body.getProfilePic()); UserDTO user = bo.getUser(id); fileService.deleteFile(user.getProfilePicture()); } UserDTO dto = new UserDTO(); dto.setId(body.getId()); dto.setUsername(body.getUsername()); dto.setEmail(body.getEmail()); dto.setShortDescription(body.getShortDes()); dto.setProfilePicture(filePath); dto.setContactNum(body.getPhoneNum()); bo.updateUserNormalDetails(dto); return new ResponseEntity<>(dto, HttpStatus.CREATED); } catch (NoSuchElementException e) { return new ResponseEntity<>("No user is found !!", HttpStatus.NOT_FOUND); } catch (Exception e) { return new ResponseEntity<>("Something went wrong !!", HttpStatus.INTERNAL_SERVER_ERROR); } }
相关截图
- Postman正常请求参考:

- 浏览器侧异常请求截图1:

- 浏览器侧异常请求截图2:

后端告警日志
2022-07-04 10:44:40.619 WARN 3068 --- [nio-8080-exec-7] o.springframework.validation.DataBinder : Skipping URI variable 'id' because request contains bind value with same name.
根因分析
- 核心403原因:Spring Boot 项目如果集成了 Spring Security,默认开启 CSRF 防护、且未配置跨域(CORS)放行规则。浏览器发送跨域非简单请求(当前PUT + multipart/form-data 属于非简单请求)时,会先发送 OPTIONS 预检请求,未配置放行规则时预检请求直接被拦截返回403;Postman 不受浏览器同源策略限制,不会发送预检请求,也不会被CSRF规则拦截,因此可以正常请求。
- 参数绑定问题:前端 FormData 中重复传入了和路径变量同名的
id字段,触发 Spring DataBinder 绑定告警,会导致路径变量id被跳过,后续参数校验逻辑可能出现异常。
修复步骤
1. 后端配置跨域与安全规则放行
在 Spring Security 配置类中添加跨域配置,放行OPTIONS预检请求,前后端分离场景可按需关闭CSRF防护:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // 前后端分离无Cookie会话场景可直接关闭CSRF,如需保留CSRF请单独配置接口白名单与Token校验逻辑 .csrf().disable() .authorizeRequests() // 放行所有OPTIONS预检请求 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 其余接口按业务需求配置权限规则 .anyRequest().authenticated() .and() // 启用跨域配置 .cors().configurationSource(corsConfigurationSource()); } private CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); // 替换为前端实际部署的域名端口,如需携带Cookie不可配置为* config.setAllowedOrigins(List.of("http://localhost:4200")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } }
如果项目没有集成Spring Security,可直接在Controller类或接口方法上添加@CrossOrigin注解临时解决跨域,生产环境建议统一配置全局CORS规则。
2. 修复前端参数重复传值问题
删除前端FormData中重复追加的id字段,id已经通过请求路径传递,无需在请求体中重复传入:
updateUser(userId: string, profilePic: string, shortDes: string, username: string, email: string, phoneNumber: string): Observable<any> { const body: FormData = new FormData(); // 移除重复的id参数追加逻辑 body.append('username', username); body.append('email', email); body.append('shortDes', shortDes); body.append('profilePic', profilePic); body.append('phoneNum', phoneNumber); return this.http.put<HttpResponse<any>>(environment.baseUrl + `/api/v1/users/` + userId, body,{ observe: 'response' }); }
补充注意
如果profilePic字段是上传的头像文件,不要传文件路径字符串,直接将文件选择框获取到的File对象append到FormData中,浏览器出于安全限制无法获取本地文件的真实路径,传字符串会导致后端无法接收到正确的文件内容。
内容的提问来源于stack exchange,提问作者Damika
相关产品推荐
相关产品推荐

