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

Railway部署前后端后遇CORS错误及/api/goals接口403问题求助

部署后CORS错误及/api/goals/**接口403问题排查

问题背景

我将React前端和SpringBoot后端子文件夹分别部署在Railway平台,更新前端部署源后出现了CORS错误。前端已在Railway中设置环境变量REACT_APP_API=https://finance-app-production-8d91.up.railway.app/api。使用Postman测试API时,除/api/goals/**接口返回403错误外,其余接口均可正常工作。

Spring配置代码

DataRestConfig配置类

@Configuration
public class DataRestConfig implements RepositoryRestConfigurer {
    private String theAllowedOrigins = "https://finance-app-frontend-production-7ab9.up.railway.app";
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) {
        HttpMethod[] theUnsupportedActions = {HttpMethod.PATCH};
        config.exposeIdsFor(Expense.class);
        config.exposeIdsFor(Goal.class);
        // disable HTTP methods
        disableHttpMethods(Expense.class, config, theUnsupportedActions);
        disableHttpMethods(Goal.class, config, theUnsupportedActions);

//        Configure CORS Mapping
        cors.addMapping(config.getBasePath() + "/**")
                .allowedOrigins(theAllowedOrigins).allowedMethods("GET", "POST", "DELETE", "PUT");
    }

    private void disableHttpMethods(Class theClass, RepositoryRestConfiguration config, HttpMethod[] theUnsupportedActions) {
        config.getExposureConfiguration()
                .forDomainType(theClass)
                .withItemExposure((metadata, httpMethods) -> httpMethods.disable(theUnsupportedActions))
                .withCollectionExposure((metadata, httpMethods) -> httpMethods.disable(theUnsupportedActions));
    }
}

SecurityConfiguration配置类

package com.example.demo.config;
import com.okta.spring.boot.oauth.Okta;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;


@Configuration
public class SecurityConfiguration {


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        // Disable cross site request forgery
        http.csrf(csrf -> csrf.disable())
                .cors(cors -> {
                    cors.configurationSource(corsConfigurationSource());
                }

                )

                // Protect endpoints at /api/<type>/secure
                .authorizeHttpRequests(auth -> auth
                                .requestMatchers("/api/expenses/**").authenticated()
                                .requestMatchers("/api/goals/**").authenticated()
                )


                .oauth2ResourceServer((oauth2) -> oauth2
                        .jwt(Customizer.withDefaults())
                )

                // Add content negotiation strategy
                .setSharedObject(ContentNegotiationStrategy.class,
                        new HeaderContentNegotiationStrategy());
        Okta.configureResourceServer401ResponseBody(http);
        return http.build(); 
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedOrigin("https://finance-app-frontend-production-7ab9.up.railway.app");
//        configuration.addAllowedMethod("*");
        configuration.setAllowedMethods(Arrays.asList("GET","POST", "PUT", "DELETE", "PATCH"));
        configuration.addAllowedHeader("*");
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new
                UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }


}

控制器示例代码

ExpenseController

@CrossOrigin("https://finance-app-frontend-production-7ab9.up.railway.app")
@RestController
@RequestMapping("/api/expenses")
public class ExpenseController {
    private ExpenseService expenseService;

    @Autowired
    public ExpenseController(ExpenseService expenseService) {
        this.expenseService = expenseService;
    }

    @PostMapping("/add")
    public void addExpense(@RequestHeader(value="Authorization") String token,
                           @RequestBody AddExpenseRequest addExpenseRequest) throws Exception {
        String userEmail = ExtractJWT.payloadJWTExtract(token, "\"sub\"");
        if (userEmail==null) {
            throw new Exception("User email is missing");
        }
        expenseService.addExpense(userEmail, addExpenseRequest);
    }
}

GoalController

@CrossOrigin("https://finance-app-frontend-production-7ab9.up.railway.app")
@RestController
@RequestMapping("/api/goals")
public class GoalController {
    private GoalService goalService;

    @Autowired
    public GoalController(GoalService goalService) {
        this.goalService = goalService;
    }

    @PostMapping("/add")
    public void addGoal(@RequestHeader(value = "Authorization") String token,
                        @RequestBody AddGoalRequest addGoalRequest) throws Exception {
        String userEmail = ExtractJWT.payloadJWTExtract(token, "\"sub\"");
        if (userEmail == null) {
            throw new Exception("User email is missing");
        }
         goalService.addGoal(userEmail,addGoalRequest);
    }
}

解决思路

  • 统一CORS配置,消除规则冲突:当前存在三处CORS配置(DataRestConfig、SecurityConfiguration、控制器@CrossOrigin),多源配置易导致规则冲突。建议保留SecurityConfiguration中的全局CORS配置,移除所有控制器上的@CrossOrigin注解,同时确保DataRestConfig中的CORS规则与全局配置一致,比如允许的源、请求方法完全匹配。

  • **定位/api/goals/的403根源:

    • 校验JWT令牌有效性:Postman测试时,确认Authorization请求头格式为Bearer <token>,且令牌解析后的sub字段存在有效邮箱,可通过JWT解码工具验证令牌内容。
    • 检查授权逻辑一致性:对比/api/expenses/**和/api/goals/**的授权规则,当前Security配置中二者均要求authenticated,需确认Goal相关接口的JWT解析流程是否正常,是否存在令牌解析异常。
    • 排查业务层权限逻辑:检查GoalService.addGoal方法是否包含额外的权限校验逻辑,比如用户是否被限制操作目标资源。
    • 开启Spring Security日志:在application.properties中添加logging.level.org.springframework.security=DEBUG,跟踪认证授权全过程,明确403错误的触发阶段。
  • Railway环境验证:

    • 确认前端实际域名:更新部署后,Railway可能会分配新的前端域名,需核对前端实际访问地址是否与配置中的https://finance-app-frontend-production-7ab9.up.railway.app一致,若有变更需同步修改所有CORS配置中的允许源。
    • 检查后端服务状态:确认后端服务正常启动,/api/goals接口无路由冲突,可通过Railway控制台查看服务日志是否有异常信息。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 15:08:11