spring-cloud / spring-cloud-gateway

An API Gateway built on Spring Framework and Spring Boot providing routing and more.
http://cloud.spring.io
Apache License 2.0
4.46k stars 3.28k forks source link

Doubled CORS headers after upgrade to Greenwich #728

Closed vpavlyuk closed 5 years ago

vpavlyuk commented 5 years ago

Some of the legacy back ends behind our gateway have their own CORS filters. On non-OPTIONS requests, they return their own Access-Control-Allow-Origin and Access-Control-Allow-Credentials response headers. With Finchley train, the gateway's CORS processor would not duplicate these response headers. After upgrade to Greenwich BUILD-SNAPSHOT and Spring Boot 2.1.1.Release, these response headers are in two copies each, rightfully upsetting the consumer (Angular based) apps:

Access to XMLHttpRequest at 'https://XXX' from origin 'https://YYY' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values 'https://YYY, https://YYY', but only one is allowed.

I will dedup those with a global post filter for now, but still thought to file this issue as the behavior changed and perhaps can be fixed at the source. Here is an integration test snippet I have that succeeds with Finchley train but fails with Greenwich:

@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("integration")
public class CorsHeadersUniquenessTest {

    @LocalServerPort
    private int port;

    private WebTestClient webClient;
    private String baseUri;

    private static ClientAndServer mockRest;

    @BeforeClass
    public static void startServer() {
        mockRest = ClientAndServer.startClientAndServer(38492);
        mockRest.when(request().withMethod("GET").withPath("/v2/version"))
                .respond(response().withStatusCode(200)
                        .withHeader("Access-Control-Allow-Credentials", "true")
                        .withHeader("Access-Control-Allow-Origin", "http://wat.com"));
    }

    @AfterClass
    public static void stopServer() {
        mockRest.stop();
    }

    @Before
    public void setup() {
        baseUri = "http://localhost:" + port;
        this.webClient = WebTestClient.bindToServer().responseTimeout(Duration.ofSeconds(3)).baseUrl(baseUri).build();
    }

    /*
    Some of the backends implement their own CORS filters, returning the response headers below.
    As we were upgrading SCG from Finchley to Greenwich, we got those response headers doubled.
    This test is here to capture the error.
    Note that OPTIONS (a.k.a. preflight) requests are fine - they never reach backends.
    All the other methods do.
     */
    @Test
    public void testCors() {
        webClient.get().uri("/v2/version")
                .header("Access-Control-Request-Method", "GET")
                .header("Origin", "http://wat.com")
        .exchange()
                .expectStatus().isOk()
                .expectHeader().valueEquals("Access-Control-Allow-Origin", "http://wat.com")
                .expectHeader().valueEquals("Access-Control-Allow-Credentials", "true")
        ;
    }

}
vpavlyuk commented 5 years ago

Forgot to provide the gateway CORS configuration. The issue can be see even with 'allow all' configuration, no matter programmatic or declared in application.xml:

    @Bean
    public CorsConfiguration corsConfiguration(RoutePredicateHandlerMapping routePredicateHandlerMapping) {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedOrigins(Collections.unmodifiableList(Arrays.asList(CorsConfiguration.ALL)));
        corsConfiguration.setAllowedMethods(Arrays.asList(
                HttpMethod.POST.name(),
                HttpMethod.GET.name(),
                HttpMethod.OPTIONS.name(),
                HttpMethod.DELETE.name(),
                HttpMethod.PUT.name(),
                HttpMethod.PATCH.name()
        ));
        corsConfiguration.setAllowedHeaders(
                Arrays.asList(ORIGIN, X_REQUESTED_WITH, CONTENT_TYPE, ACCEPT, AUTHORIZATION, "XXX"));
        corsConfiguration.setMaxAge(3600L);
        corsConfiguration.setAllowCredentials(true);
        routePredicateHandlerMapping.setCorsConfigurations(
                new HashMap<String, CorsConfiguration>() {{ put("/**", corsConfiguration); }});
        return corsConfiguration;
    }
spencergibb commented 5 years ago

That test doesn't fail for me.

vpavlyuk commented 5 years ago

Please use https://github.com/vpavlyuk/spring-cloud-gateway-sample to reproduce.

java.lang.AssertionError: Response header 'Access-Control-Allow-Origin' expected:<[http://wat.com]> but was:<[http://wat.com, http://wat.com]>

Then downgrade in pom.xml to spring-boot-starter-parent version 2.0.5.RELEASE, and to Finchley.BUILD-SNAPSHOT. The test passes.

spencergibb commented 5 years ago

It's not complete since there's no downstream service to connect to.

vpavlyuk commented 5 years ago

@spencergibb I added a route to httpbin.org to reproduce manually and updated the sample. https://github.com/vpavlyuk/spring-cloud-gateway-sample#manually-with-httpbinorg-backend

spencergibb commented 5 years ago

I see it happen, but I think it wasn't on purpose that it didn't happen Finchley, so I'm not sure it is a regression. If anything, it might be a bug that it didn't add them. You've configured spring boot (running the gateway) to add the headers and your downstream service adds them. There's no gateway specific code that adds them, we just plug into spring framework and it does it. I think the right thing is to do what you are doing and dedupe on your legacy services.

vpavlyuk commented 5 years ago

OK, thanks, feel free to close this. If it'd make sense I can contribute the deduping filter to SCG.

spencergibb commented 5 years ago

PRs welcome

crazyman2010 commented 5 years ago

I have the same question, if the downstream service write the Access-Control-Allow-Origin:x , when return to the client , it will be Access-Control-Allow-Origin: x, *, and the browser not working. finally I add a global filter after NettyWriteResponseFilter to fix the bad header.

vpavlyuk commented 5 years ago

I'll contribute my dedupe filter to SCG once I find time to brush it up.

vpavlyuk commented 5 years ago

Created PR https://github.com/spring-cloud/spring-cloud-gateway/pull/866 with a generic dedupe filter.

howardem commented 5 years ago

Same thing happening here :( We are trying to migrate our current Spring Cloud Zuul gateway to the newest Spring Cloud Gateway (v2.1.0.RELEASE) based on Webflux, and the browser client is having the same issues with duplicated CORS Access-Control-Allow-Origin response header.

swarnar87 commented 4 years ago

Still facing the issue with duplicate origins in Access-Control-Allow-Origin , even though the last comment says it has been fixed with Greenwich.SR2 release. Can anybody tell me the workaround or the proper version to be used?

dmonti commented 4 years ago

I'm facing this weird behavior too, when no 'Access-Control-Allow-Origin' is added: No 'Access-Control-Allow-Origin' header is present on the requested resource.

But if any 'Access-Control-Allow-Origin' is set, then my response come with an extra The 'Access-Control-Allow-Origin' header contains multiple values ', ', but only one is allowed. or The 'Access-Control-Allow-Origin' header contains multiple values 'http://localhost:8080, ', but only one is allowed.

Version: Greenwich.SR2

swarnar87 commented 4 years ago

Check if the upstream application has CORS configuration setup too. That was causing this issue for me. I removed it and the issue was gone. If you do not have control on the upstream application, try using the dedupe filter (check spring io docs for spring cloud gateway).

dmonti commented 4 years ago

Thanx @swarnar87 , I forgot to remove @CrossOrigin from my upstream application controllers

luohaoGit commented 4 years ago

Thanx 42

gaochundong commented 4 years ago

nice

GnanaJeyam commented 4 years ago

This works for me No need to add any bean.

spring:
   cloud:    
    gateway:
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE
      globalcors:
          cors-configurations:
             '[/**]':
             allowed-origins: "*"
             allowed-methods: "*"
             allowed-headers: "*"
             allow-credentials: true
Mbd06b commented 4 years ago

This works for me No need to add any bean.

spring:
   cloud:    
    gateway:
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE
      globalcors:
          cors-configurations:
             '[/**]':
             allowed-origins: "*"
             allowed-methods: "*"
             allowed-headers: "*"
             allow-credentials: true

This almost worked for me,

The RETAIN_UNIQUE parameter will retain every unique header that is added along the way. So my client(localhost:4200) and browser was still reporting duplicate headers with values "http://localhost:4200" and "*".

I decided to be explicit with the allowed-origins: "http://localhost:4200" which ensured they matched. This will still allow the browser to throw CORS errors if any other origin is used in your server, which is probably good from a security standpoint.

Learn more about setting gateway filter factories here... https://cloud.spring.io/spring-cloud-gateway/2.1.x/multi/multi__gatewayfilter_factories.html

muzuro commented 3 years ago

This works for me No need to add any bean.

spring:
   cloud:    
    gateway:
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE
      globalcors:
          cors-configurations:
             '[/**]':
             allowed-origins: "*"
             allowed-methods: "*"
             allowed-headers: "*"
             allow-credentials: true

I have applied following configuration but still receive error:

Access to XMLHttpRequest at 'http://localhost:8080/xx/yy?' from origin 'http://localhost:57254' 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.
Jeyam-Ideas2it commented 3 years ago

This works for me No need to add any bean.

spring:
   cloud:    
    gateway:
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE
      globalcors:
          cors-configurations:
             '[/**]':
             allowed-origins: "*"
             allowed-methods: "*"
             allowed-headers: "*"
             allow-credentials: true

I have applied following configuration but still receive error:

Access to XMLHttpRequest at 'http://localhost:8080/xx/yy?' from origin 'http://localhost:57254' 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.

Make sure you getting the cross origin response header from your downstream services. As per your comment its does get the cors header No 'Access-Control-Allow-Origin' header is present on the requested resource.

xzxiaoshan commented 3 years ago
    /*
    Default: Retain the first value only.
     */
    RETAIN_FIRST,

    /*
    Retain the last value only.
     */
    RETAIN_LAST,

    /*
    Retain all unique values in the order of their first encounter.
     */
    RETAIN_UNIQUE
imcvakt commented 3 years ago

This works for me No need to add any bean.

spring:
   cloud:    
    gateway:
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE
      globalcors:
          cors-configurations:
             '[/**]':
             allowed-origins: "*"
             allowed-methods: "*"
             allowed-headers: "*"
             allow-credentials: true

That works for me!

jzdayz commented 3 years ago

Check if the upstream application has CORS configuration setup too. That was causing this issue for me. I removed it and the issue was gone. If you do not have control on the upstream application, try using the dedupe filter (check spring io docs for spring cloud gateway).

Thanks,work for me

chibexme commented 3 years ago
    /*
    Default: Retain the first value only.
     */
    RETAIN_FIRST,

    /*
    Retain the last value only.
     */
    RETAIN_LAST,

    /*
    Retain all unique values in the order of their first encounter.
     */
    RETAIN_UNIQUE

RETAIN_FIRST worked for me. Thanks everyone.

rcbandit111 commented 1 year ago

In Spring Cloud Gateway 2022.0.3 I added:

spring:
   cloud:    
    gateway:
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE
      globalcors:
          cors-configurations:
             '[/**]':
             allowed-origins: "*"
             allowed-methods: "*"
             allowed-headers: "*"
             allow-credentials: true

Error During startup:

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to bind properties under 'spring.cloud.gateway.globalcors.cors-configurations[/**]' to org.springframework.web.cors.CorsConfiguration:

    Reason: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [org.springframework.web.cors.CorsConfiguration]

Action:

Update your application's configuration

Any solution?

Mbd06b commented 1 year ago

In Spring Cloud Gateway 2022.0.3 I added:

spring:
   cloud:    
    gateway:
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE
      globalcors:
          cors-configurations:
             '[/**]':
             allowed-origins: "*"
             allowed-methods: "*"
             allowed-headers: "*"
             allow-credentials: true

Error During startup:

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to bind properties under 'spring.cloud.gateway.globalcors.cors-configurations[/**]' to org.springframework.web.cors.CorsConfiguration:

    Reason: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [org.springframework.web.cors.CorsConfiguration]

Action:

Update your application's configuration

Any solution?

Just from what you posted it sounds like '[/**]' isn't placed in a valid location. I think it's reading that as a string than getting your cors properties below.

rcbandit111 commented 1 year ago

In Spring Cloud Gateway 2022.0.3 I added:

spring:
   cloud:    
    gateway:
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE
      globalcors:
          cors-configurations:
             '[/**]':
             allowed-origins: "*"
             allowed-methods: "*"
             allowed-headers: "*"
             allow-credentials: true

Error During startup:

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to bind properties under 'spring.cloud.gateway.globalcors.cors-configurations[/**]' to org.springframework.web.cors.CorsConfiguration:

    Reason: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [org.springframework.web.cors.CorsConfiguration]

Action:

Update your application's configuration

Any solution?

Just from what you posted it sounds like '[/**]' isn't placed in a valid location. I think it's reading that as a string than getting your cors properties below.

Yes, I saw it and I tested it but I the configuration is not working.

spencergibb commented 1 year ago

@rcbandit111 it's best not to comment on an unrelated issue. Please open a new one with a complete, minimal, verifiable sample (something that we can unzip attached to the new issue or git clone, build, and deploy) that reproduces the problem.