Spring Boot微服务+Vue经API网关请求报CORS跨域错误
问题背景
我正在开发后端采用Spring Boot微服务架构、前端采用Vue.js的项目,项目结构如下:
此前在控制器类上添加@CrossOrigin注解即可规避CORS错误,项目一直运行正常,直到新增支持用户登录的安全模块后出现跨域问题。
已完成配置
- 在基于
spring-cloud-gateway搭建的API网关中新增AuthFilter拦截器,用于JWT的创建与校验,文件路径:api-gateway/src/main/java/.../AuthFilter.java
- 在基于
@Component public class AuthFilter extends AbstractGatewayFilterFactory<AuthFilter.Config> { private final WebClient.Builder webClientBuilder; @Autowired public AuthFilter(WebClient.Builder webClientBuilder) { super(Config.class); this.webClientBuilder = webClientBuilder; } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { if(!exchange.getRequest().getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) { throw new RuntimeException("Missing auth information"); } String authHeader = exchange.getRequest().getHeaders().get(org.springframework.http.HttpHeaders.AUTHORIZATION).get(0); String[] parts = authHeader.split(" "); if(parts.length != 2 || !"Bearer".equals(parts[0])) { throw new RuntimeException("Incorrect auth structure"); } return webClientBuilder.build() .post() .uri("http://manager-service/api/v1/auth/validateToken?token=" + parts[1]) .retrieve() .bodyToMono(EmployeeDTO.class) //EmployeeDTO.class is custom DTO that represents User .map(user -> { exchange.getRequest() .mutate() .header("x-auth-user-id", user.getId()); return exchange; }).flatMap(chain::filter); }; } public static class Config { //留空,无需额外配置 } }
- 在网关的
application.properties中为各服务路由绑定AuthFilter过滤器,文件路径:api-gateway/src/resource/application.properties
- 在网关的
##Workshop service routes spring.cloud.gateway.routes[0].id=workshop-service spring.cloud.gateway.routes[0].uri=lb://workshop-service spring.cloud.gateway.routes[0].predicates[0]=Path=/api/v1/workshop/** spring.cloud.gateway.routes[0].filters[0]=AuthFilter ##Manage service routes spring.cloud.gateway.routes[1].id=manager-service spring.cloud.gateway.routes[1].uri=lb://manager-service spring.cloud.gateway.routes[1].predicates[0]=Path=/api/v1/manage/** spring.cloud.gateway.routes[1].filters[0]=AuthFilter ##登录接口路由,无需挂载AuthFilter,对所有用户开放 spring.cloud.gateway.routes[2].id=manager-service-sign-in spring.cloud.gateway.routes[2].uri=lb://manager-service spring.cloud.gateway.routes[2].predicates[0]=Path=/api/v1/auth/signIn ...
- manager-service微服务负责用户、角色、组织等基础实体管控,承担JWT生成职责,在该服务中新增了
SecurityConfig和WebConfig配置
SecurityConfig.java代码:
@EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity .csrf().disable() .authorizeRequests().anyRequest().permitAll(); return httpSecurity.build(); } }WebConfig.java代码:
@EnableWebMvc public class WebConfig implements WebMvcConfigurer { private static final Long MAX_AGE=3600L; @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedHeaders( HttpHeaders.AUTHORIZATION, HttpHeaders.CONTENT_TYPE, HttpHeaders.ACCEPT) .allowedMethods( HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.PUT.name(), HttpMethod.DELETE.name()) .maxAge(MAX_AGE) .allowedOrigins("http://localhost:8100") .allowCredentials(false); } }- manager-service微服务负责用户、角色、组织等基础实体管控,承担JWT生成职责,在该服务中新增了
- 在鉴权控制器类上添加了
@CrossOrigin注解,文件路径:manager-service/src/main/java/.../AuthController.java
- 在鉴权控制器类上添加了
@RestController @RequestMapping("api/v1/auth") @CrossOrigin(origins = "http://localhost:8100") @Slf4j public class AuthController { private final AuthService authService; @Autowired public AuthController(AuthService authService) { this.authService = authService; } @PostMapping("/signIn") public ResponseEntity<EmployeeDTO> signIn(@RequestBody CredentialsDTO credentialsDTO) { log.info("Trying to login {}", credentialsDTO.getLogin()); return ResponseEntity.ok(EmployeeMapper.convertToDTO(authService.signIn(credentialsDTO))); } @PostMapping("/validateToken") public ResponseEntity<EmployeeDTO> validateToken(@RequestParam String token) { log.info("Trying to validate token {}", token); Employee validatedTokenUser = authService.validateToken(token); return ResponseEntity.ok(EmployeeMapper.convertToDTO(validatedTokenUser)); } }
- 前端Vue.js项目通过
axios发起请求,登录POST请求代码如下:
- 前端Vue.js项目通过
axios.post('http://localhost:8080/api/v1/auth/signIn', this.credentials).then(response => { console.log('response = ', response) console.log('token from response', response.data.token) this.$store.commit('saveToken', response.data.token) }).catch(error => { console.log('Error is below') console.log(error) })
问题现象
所有请求均触发如下错误:Access to XMLHttpRequest at 'http://localhost:8080/api/v1/auth/signIn' from origin 'http://localhost:8100' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Chrome中展示的请求头如下:
此前尝试新增额外corsConfiguration配置、将类上的@CrossOrigin注解移到方法上,均未生效。使用Postman发起请求可正常返回令牌。
排查进展
经定位问题出在api-gateway层:直接请求后端服务可得到正确响应,经过网关转发时就会报错,网关日志如下:
2022-07-05 00:34:18.128 TRACE 8105 --- [or-http-epoll-5] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "[/api/v1/workshop/**]" does not match against value "/api/v1/auth/signIn" 2022-07-05 00:34:18.129 TRACE 8105 --- [or-http-epoll-5] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "[/api/v1/manage/**]" does not match against value "/api/v1/auth/signIn" 2022-07-05 00:34:18.129 TRACE 8105 --- [or-http-epoll-5] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "/api/v1/auth/signIn" matches against value "/api/v1/auth/signIn" 2022-07-05 00:34:18.129 DEBUG 8105 --- [or-http-epoll-5] o.s.c.g.h.RoutePredicateHandlerMapping : Route matched: manager-service-sign-in 2022-07-05 00:34:18.129 DEBUG 8105 --- [or-http-epoll-5] o.s.c.g.h.RoutePredicateHandlerMapping : Mapping [Exchange: OPTIONS http://localhost:8080/api/v1/auth/signIn] to Route{id='manager-service-sign-in', uri=lb://manager-service, order=0, predicate=Paths: [/api/v1/auth/signIn], match trailing slash: true, gatewayFilters=[], metadata={}} 2022-07-05 00:34:18.129 DEBUG 8105 --- [or-http-epoll-5] o.s.c.g.h.RoutePredicateHandlerMapping : [e5b87280-8] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler@78df1cfc
问题根因
- 跨域配置位置错误:Spring Cloud Gateway是基于WebFlux的响应式网关,作为所有请求的统一入口,浏览器发起的OPTIONS预检请求会先到达网关,下游微服务配置的
@CrossOrigin、WebConfig跨域规则根本不会生效。网关本身没有配置CORS规则时,预检请求拿不到合法的跨域响应头就会被拦截。 - 鉴权过滤器逻辑缺陷:浏览器发送OPTIONS预检请求时不会携带
Authorization请求头,现有AuthFilter逻辑会直接抛出缺失鉴权信息的异常,导致请求直接失败,无法返回正确的跨域头。
解决方案
1. 网关层统一配置全局CORS
删除下游所有微服务里的跨域配置(包括WebConfig类、控制器上的@CrossOrigin注解),避免跨域头重复返回导致新问题,在API网关层统一处理跨域,以下两种配置方式二选一即可:
- 配置文件方式(在网关的application.properties中添加):
spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedOrigins=http://localhost:8100 spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedHeaders=* spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedMethods=GET,POST,PUT,DELETE,OPTIONS spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowCredentials=false spring.cloud.gateway.globalcors.corsConfigurations.[/**].maxAge=3600
- Java配置类方式(在网关服务中新增配置类):
@Configuration public class GatewayCorsConfig { @Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("http://localhost:8100")); config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS")); config.addAllowedHeader("*"); config.setAllowCredentials(false); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); } }
注意:网关是WebFlux运行环境,跨域配置要使用
CorsWebFilter,不要用Spring MVC的WebMvcConfigurer,否则配置不会生效。
2. 修改鉴权过滤器逻辑,放行OPTIONS请求
在AuthFilter的逻辑最前面添加OPTIONS请求放行判断,避免预检请求被鉴权逻辑拦截:
@Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { // 直接放行OPTIONS预检请求 if (exchange.getRequest().getMethod() == HttpMethod.OPTIONS) { return chain.filter(exchange); } // 原有鉴权逻辑保持不变 if(!exchange.getRequest().getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) { throw new RuntimeException("Missing auth information"); } String authHeader = exchange.getRequest().getHeaders().get(org.springframework.http.HttpHeaders.AUTHORIZATION).get(0); String[] parts = authHeader.split(" "); if(parts.length != 2 || !"Bearer".equals(parts[0])) { throw new RuntimeException("Incorrect auth structure"); } return webClientBuilder.build() .post() .uri("http://manager-service/api/v1/auth/validateToken?token=" + parts[1]) .retrieve() .bodyToMono(EmployeeDTO.class) .map(user -> { exchange.getRequest() .mutate() .header("x-auth-user-id", user.getId()); return exchange; }).flatMap(chain::filter); }; }
配置完成后重启网关服务即可解决跨域问题。
内容的提问来源于stack exchange,提问作者Allteran

