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

Spring API Gateway全局CORS错误:Angular登录请求失败

问题描述

基于Spring Security搭建用户管理微服务,搭配Spring Cloud Gateway做路由转发,Postman调用登录接口正常,但本地Angular应用发起登录请求时触发CORS错误。


相关配置代码

1. Spring Security配置

@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
    // Configure AuthenticationManagerBuilder
    AuthenticationManagerBuilder authenticationManagerBuilder =
            http.getSharedObject(AuthenticationManagerBuilder.class);

    authenticationManagerBuilder.userDetailsService(userDetailsService)
            .passwordEncoder(bCryptPasswordEncoder);
    AuthenticationManager authenticationManager = authenticationManagerBuilder.build();

    // Create AuthenticationFilter
    AuthenticationFilter authenticationFilter = getAuthenticationFilter(authenticationManager);

    IpAddressMatcher hasIpAddress = new IpAddressMatcher(Objects.requireNonNull(environment.getProperty("gateway.ip")));
    IpAddressMatcher hasIpv4Address = new IpAddressMatcher(Objects.requireNonNull(environment.getProperty("gateway.ipv4")));

    return http
            .cors(corsCustomizer -> corsCustomizer.configurationSource(corsConfigurationSource()))
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/**")
                            .access((authentication, context) -> getAccess(authentication, context, hasIpAddress, hasIpv4Address))
                    .requestMatchers(new AntPathRequestMatcher("/actuator/**"))
                    .access((authentication, context) -> getAccess(authentication, context, hasIpAddress, hasIpv4Address))
                    .requestMatchers(HttpMethod.POST, SecurityConstants.SIGN_UP_URL)
                    .access((authentication, context) -> getAccess(authentication, context, hasIpAddress, hasIpv4Address))
                    .requestMatchers(HttpMethod.GET, SecurityConstants.EMAIL_VERIFICATION_URL)
                    .access((authentication, context) -> getAccess(authentication, context, hasIpAddress, hasIpv4Address))
                    .requestMatchers(HttpMethod.POST, SecurityConstants.PASSWORD_RESET_REQUEST_URL)
                    .access((authentication, context) -> getAccess(authentication, context, hasIpAddress, hasIpv4Address))
                    .requestMatchers(HttpMethod.POST, SecurityConstants.PASSWORD_RESET_URL)
                    .access((authentication, context) -> getAccess(authentication, context, hasIpAddress, hasIpv4Address))
                    .requestMatchers("/v2/api-docs", "/configuration/**", "/swagger*/**", "/webjars/**", "/v3/api-docs/**").permitAll()
                    .requestMatchers(HttpMethod.GET, "/hello")
                    .access((authentication, context) -> getAccess(authentication, context, hasIpAddress, hasIpv4Address))
            )
            .addFilterAfter(authenticationFilter, AuthenticationFilter.class)
            .addFilterAfter(new AuthorizationFilter(authenticationManager, jwtTokenProvider), AuthorizationFilter.class)
            .authenticationManager(authenticationManager)
            .sessionManagement((session) -> session
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .build();
}

2. Spring Cloud Gateway全局CORS配置

# https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/cors-configuration.html
spring.cloud.gateway.default-filters= DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
spring.cloud.gateway.globalcors.cors-configurations[/**].allowed-origins="http://localhost:4200, http://127.0.0.1:4200"
spring.cloud.gateway.globalcors.cors-configurations[/**].allowed-headers="*"
spring.cloud.gateway.globalcors.cors-configurations[/**].allowedMethods="POST", "GET", "PUT", "DELETE", "PATCH", "OPTIONS"
spring.cloud.gateway.globalcors.cors-configurations[/**].max-age=3600
spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping=true

3. Gateway路由配置

# user registration routes do not need filter
spring.cloud.gateway.routes[0].id=users-ws
spring.cloud.gateway.routes[0].uri = lb://users-ws
spring.cloud.gateway.routes[0].predicates[0]=Path=/users-ws/users
spring.cloud.gateway.routes[0].predicates[1]=Method=POST
spring.cloud.gateway.routes[0].filters[0]=RemoveRequestHeader=Cookie

spring.cloud.gateway.routes[1].id = users-ws-login
spring.cloud.gateway.routes[1].uri = lb://users-ws
spring.cloud.gateway.routes[1].predicates[0]=Path=/users-ws/users/login
spring.cloud.gateway.routes[1].predicates[1]=Method=POST
spring.cloud.gateway.routes[1].filters[0]=RemoveRequestHeader=Cookie

# user management routes do need filter
spring.cloud.gateway.routes[2].id=users-management-ws
spring.cloud.gateway.routes[2].uri=lb://users-ws
spring.cloud.gateway.routes[2].predicates[0]=Path=/users-ws/users/**
spring.cloud.gateway.routes[2].predicates[1]=Method=GET,PUT,DELETE
spring.cloud.gateway.routes[2].predicates[2]=Header=Authorization, Bearer (.*)
spring.cloud.gateway.routes[2].filters[0]=RemoveRequestHeader=Cookie
spring.cloud.gateway.routes[2].filters[1]=AuthorizationHeaderFilter

# configuring filter for actuator
spring.cloud.gateway.routes[3].id = users-ws-actuator
spring.cloud.gateway.routes[3].uri = lb://users-ws
spring.cloud.gateway.routes[3].predicates[0]=Path=/users-ws/actuator/**
spring.cloud.gateway.routes[3].predicates[1]=Method=GET
spring.cloud.gateway.routes[3].filters[0]=RemoveRequestHeader=Cookie

4. 自定义AuthorizationHeaderFilter

@Component
public class AuthorizationHeaderFilter
        extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
    private final Environment env;

    @Autowired
    public AuthorizationHeaderFilter(Environment env) {
        super(Config.class);
        this.env = env;
    }

    @Override
    public GatewayFilter apply(Config config) {
        return ((exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            if(!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION))
                return onError(exchange, " No authorization Header", HttpStatus.UNAUTHORIZED);
            String authHeader = Objects.requireNonNull(
                    request.getHeaders().get(HttpHeaders.AUTHORIZATION)).get(0);
            String jwt = authHeader.replace("Bearer ", "");

            if(!isJwtValid(jwt))
                return onError(exchange, "Invalid JWT", HttpStatus.UNAUTHORIZED);

            return chain.filter(exchange);
        });
    }

    private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);
        return response.setComplete();
    }

    private boolean isJwtValid(String jwt) {
        boolean isValid = true;
        String subject = null;
        String tokenSecret = env.getProperty("token.secret");

        assert tokenSecret != null;
        byte[] tokenSecretBytes = Base64.getEncoder().encode(tokenSecret.getBytes());
        SecretKey signingKey = Keys.hmacShaKeyFor(tokenSecretBytes);

        JwtParser parser = Jwts.parser()
                .verifyWith(signingKey)
                .build();
        try {
            subject = parser.parseSignedClaims(jwt).getPayload().getSubject();
        } catch (Exception e) {
            System.out.println(e.getMessage());
            isValid = false;
        }

        if(subject == null || subject.isEmpty()) {
            isValid = false;
        }
        return  isValid;
    }

    public static class Config {
        // put configuration properties
    }
}

5. 尝试的编程式CORS配置

@Configuration
public class AppGlobalCorsConfiguration extends CorsConfiguration {
    @Bean
    public CorsConfigurationSource corsWebFilter() {
        final CorsConfiguration corsConfig = new CorsConfiguration();
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        corsConfig.setAllowedOrigins(Arrays.asList("http://localhost:4200", "http://127.0.0.1:4200"));
        corsConfig.setMaxAge(3600L);
        corsConfig.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "DELETE", "PATCH", "OPTIONS"));
        corsConfig.setAllowedHeaders(Collections.singletonList("*"));

        source.registerCorsConfiguration("/**", corsConfig);
        return source;
    }
}

6. Angular登录请求代码

login(email: string, password: string): Observable<any> {
  return this.http.post<AuthModel>(`${API_USERS_URL}/login`, {
    email,
    password,
  });
}

错误信息

CORS错误截图

跨源请求被阻止:同源策略禁止读取位于 http://localhost:8082/users-ws/users/login 的远程资源。(原因:CORS头缺少'Access-Control-Allow-Origin')。状态码:401。


解决方案

问题根因

  1. OPTIONS预检请求未被放行:Angular发起POST请求前会自动发送OPTIONS预检请求,但当前Gateway登录路由只匹配POST方法,OPTIONS请求被拦截返回401,触发CORS错误。
  2. 多重复CORS配置冲突:同时在Gateway、微服务Security中配置CORS,导致响应头混乱;另外编程式配置类错误继承CorsConfiguration,属于无效配置。
  3. IP白名单限制:Security中所有请求都经过getAccess方法校验IP,Angular请求来自localhost,可能不在配置的gateway.ip/gateway.ipv4白名单内,导致OPTIONS请求被拒绝。

修复步骤

1. 调整Gateway路由,允许OPTIONS请求

修改登录和注册路由的Method predicate,加入OPTIONS方法:

spring.cloud.gateway.routes[0].predicates[1]=Method=POST,OPTIONS
spring.cloud.gateway.routes[1].predicates[1]=Method=POST,OPTIONS

2. 简化CORS配置,仅在Gateway层处理

  • 移除微服务Security配置中的.cors(...)代码块,避免和Gateway的CORS配置冲突。
  • 删除微服务中的AppGlobalCorsConfiguration类,微服务层无需重复配置CORS。
  • 修正Gateway的CORS配置,去掉allowed-origins的引号(引号会导致域名匹配失败):
spring.cloud.gateway.globalcors.cors-configurations[/**].allowed-origins=http://localhost:4200,http://127.0.0.1:4200

3. 调整Security规则,放行OPTIONS请求

在Security的authorizeHttpRequests中优先放行所有OPTIONS请求:

.authorizeHttpRequests(auth -> auth
        .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 先放行所有OPTIONS预检请求
        .requestMatchers("/**")
                .access((authentication, context) -> getAccess(authentication, context, hasIpAddress, hasIpv4Address))
        // 其他原有规则保持不变
)

或者在getAccess方法中判断请求方法,若为OPTIONS直接允许:

private AuthorizationDecision getAccess(Authentication authentication, AuthorizationContext context, IpAddressMatcher hasIpAddress, IpAddressMatcher hasIpv4Address) {
    HttpServletRequest request = context.getRequest();
    if (HttpMethod.OPTIONS.name().equals(request.getMethod())) {
        return new AuthorizationDecision(true);
    }
    // 原有IP校验逻辑
}

4. 优化Gateway的响应头去重配置

确保DedupeResponseHeader配置明确去重策略,避免重复的CORS响应头:

spring.cloud.gateway.default-filters=DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin, RETAIN_FIRST

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 11:14:51