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
相关产品推荐
相关产品推荐

