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

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正常请求参考:Postman请求截图
  • 浏览器侧异常请求截图1:浏览器请求截图1
  • 浏览器侧异常请求截图2:浏览器请求截图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.

根因分析

  1. 核心403原因:Spring Boot 项目如果集成了 Spring Security,默认开启 CSRF 防护、且未配置跨域(CORS)放行规则。浏览器发送跨域非简单请求(当前PUT + multipart/form-data 属于非简单请求)时,会先发送 OPTIONS 预检请求,未配置放行规则时预检请求直接被拦截返回403;Postman 不受浏览器同源策略限制,不会发送预检请求,也不会被CSRF规则拦截,因此可以正常请求。
  2. 参数绑定问题:前端 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 06:39:18