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

Angular 19调用Spring Cloud Gateway接口遇CORS及401问题求助

问题描述

我有一个基于Spring Boot微服务的应用,采用Spring Cloud Gateway向前端暴露微服务接口。尽管已配置添加Access-Control-Allow-Origin响应头,但从Angular 19 SPA调用接口时仍遭遇CORS错误。我已启用HttpOnly,由后端处理Token;登录、注册、登出接口均可正常调用,但在应用初始化时调用获取当前用户信息的接口却返回401错误。

错误信息

dashboard:1 Access to XMLHttpRequest at 'http://localhost:8084/api/1.0/users/8aad4849-21eb-4fc1-b852-83e078c2a90e' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
user-account.service.ts:21 Account API error HttpErrorResponse {headers: HttpHeaders, status: 0, statusText: 'Unknown Error', url: 'http://localhost:8084/api/1.0/users/8aad4849-21eb-4fc1-b852-83e078c2a90e', ok: false, …}error: ProgressEvent {isTrusted: true, lengthComputable: false, loaded: 0, total: 0, type: 'error', …}headers: HttpHeaders {headers: Map(0), normalizedNames: Map(0), lazyInit: undefined, lazyUpdate: null}message: "Http failure response for http://localhost:8084/api/1.0/users/8aad4849-21eb-4fc1-b852-83e078c2a90e: 0 Unknown Error"name: "HttpErrorResponse"ok: falsestatus: 0statusText: "Unknown Error"type: undefinedurl: "http://localhost:8084/api/1.0/users/8aad4849-21eb-4fc1-b852-83e078c2a90e"[[Prototype]]: HttpResponseBase

相关配置文件

API网关配置(application.yml)

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: auth-service
          uri: lb://auth-service
          predicates:
            - Path=/api/1.0/auth/**
        - id: account-service
          uri: lb://account-service
          predicates:
            - Path=/api/1.0/users/**
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins:
              - "http://localhost:4200"
            allowedMethods:
              - GET
              - POST
              - PUT
              - DELETE
              - OPTIONS
            allowedHeaders:
              - "*"
            allowCredentials: true
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8080/realms/My-Realm_Realm
server:
  port: 8084
  error:
    include-stacktrace: always
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

网关安全配置(SecurityConfig.java)

package com.omb.ombgateway.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain gatewaySecurity(ServerHttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
                .authorizeExchange(exchanges -> exchanges
                        .pathMatchers("/api/1.0/auth/**", "/actuator/**", "/error").permitAll()
                        .anyExchange().authenticated()
                )
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
        return http.build();
    }
}

账户微服务安全配置(SecurityConfig.java)

package com.omb.ombaccount.configuration;

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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter());

        http
                .csrf(csrf -> csrf.disable())
                .cors(Customizer.withDefaults())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/**").hasAnyRole("USER", "ADMIN")
                        .requestMatchers("/actuator/**", "/h2-console/**").permitAll()
                )
                .oauth2ResourceServer(oauth2 -> oauth2
                        .jwt(jwt -> jwt
                                .jwtAuthenticationConverter(jwtAuthenticationConverter)
                        )
                );
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder(16);
    }
}

账户微服务CORS配置(CorsGlobalConfiguration.java)

package com.omb.ombaccount.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

@Configuration
public class CorsGlobalConfiguration {

    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration corsConfig = new CorsConfiguration();
        corsConfig.setAllowCredentials(true);
        corsConfig.addAllowedOrigin("http://localhost:4200");
        corsConfig.addAllowedHeader("*");
        corsConfig.addAllowedMethod("GET");
        corsConfig.addAllowedMethod("POST");
        corsConfig.addAllowedMethod("PUT");
        corsConfig.addAllowedMethod("DELETE");
        corsConfig.addAllowedMethod("OPTIONS");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfig);

        return new CorsWebFilter(source);
    }
}

Angular账户HTTP客户端代码

getUserAccount(id: string): Observable<UserAccountModel> {
    return this.http.get<UserAccountModel>(this.accountBaseUrl + '/'+id, { withCredentials: true }).pipe(
      catchError(error => {
        console.error('Account API error', error);
        return throwError(() => error);
      })
    );
  }

解决方案

1. 统一CORS配置到网关,移除微服务层的CORS配置

Spring Cloud Gateway作为前端唯一入口,CORS配置应该只在网关上生效,微服务层的CORS配置会和网关的配置冲突,导致响应头异常。直接删除账户微服务中的CorsGlobalConfiguration类,保留网关的globalcors配置即可。

2. 修复401错误导致的次生CORS问题

你看到的CORS错误本质是401未授权的次生问题:当网关验证JWT失败返回401时,Spring Security的错误响应没有带上CORS头,浏览器就会判定为CORS错误。修改网关的SecurityConfig,添加异常处理确保错误响应带CORS头:

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain gatewaySecurity(ServerHttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
                .authorizeExchange(exchanges -> exchanges
                        .pathMatchers("/api/1.0/auth/**", "/actuator/**", "/error").permitAll()
                        .anyExchange().authenticated()
                )
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
                // 添加异常处理,给401响应加CORS头
                .exceptionHandling(exceptions -> exceptions
                        .authenticationEntryPoint((exchange, ex) -> {
                            ServerHttpResponse response = exchange.getResponse();
                            response.getHeaders().add("Access-Control-Allow-Origin", "http://localhost:4200");
                            response.getHeaders().add("Access-Control-Allow-Credentials", "true");
                            response.setStatusCode(HttpStatus.UNAUTHORIZED);
                            return response.setComplete();
                        })
                );
        return http.build();
    }
}

3. 验证Token传递有效性

应用初始化时的401问题,需要确认:

  • 登录后生成的HttpOnly Cookie是否包含有效JWT
  • Angular请求的withCredentials: true是否生效(查看浏览器Network标签的Request Headers是否带Cookie)
  • 网关的issuer-uri是否能正常访问Keycloak获取公钥,JWT验证逻辑是否正确

4. 调试建议

  • 开启网关的DEBUG日志,查看JWT验证失败的具体原因(如Token过期、签名无效)
  • 用Postman直接调用网关接口,排除前端请求的问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 11:10:54