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错误的触发阶段。
- 校验JWT令牌有效性:Postman测试时,确认Authorization请求头格式为
Railway环境验证:
- 确认前端实际域名:更新部署后,Railway可能会分配新的前端域名,需核对前端实际访问地址是否与配置中的
https://finance-app-frontend-production-7ab9.up.railway.app一致,若有变更需同步修改所有CORS配置中的允许源。 - 检查后端服务状态:确认后端服务正常启动,
/api/goals接口无路由冲突,可通过Railway控制台查看服务日志是否有异常信息。
- 确认前端实际域名:更新部署后,Railway可能会分配新的前端域名,需核对前端实际访问地址是否与配置中的
内容的提问来源于stack exchange,提问作者Kalamazoo
相关产品推荐
相关产品推荐

