Spring Cloud 轻松解决跨域,别再乱用了!

这篇具有很好参考价值的文章主要介绍了Spring Cloud 轻松解决跨域,别再乱用了!。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

问题

在Spring Cloud项目中,前后端分离目前很常见,在调试时,会遇到两种情况的跨域:

前端页面通过不同域名或IP访问微服务的后台,例如前端人员会在本地起HttpServer 直连后台开发本地起的服务,此时,如果不加任何配置,前端页面的请求会被浏览器跨域限制拦截,所以,业务服务常常会添加如下代码设置全局跨域:

@Bean
public CorsFilter corsFilter() {
    logger.debug("CORS限制打开");
    CorsConfiguration config = new CorsConfiguration();
    # 仅在开发环境设置为*
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");
    config.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
    configSource.registerCorsConfiguration("/**", config);
    return new CorsFilter(configSource);
}

前端页面通过不同域名或IP访问SpringCloud Gateway,例如前端人员在本地起HttpServer直连服务器的Gateway进行调试。此时,同样会遇到跨域。需要在Gateway的配置文件中增加:

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
        # 仅在开发环境设置为*
          '[/**]':
            allowedOrigins: "*"
            allowedHeaders: "*"
            allowedMethods: "*"

那么,此时直连微服务和网关的跨域问题都解决了,是不是很完美?

Spring Cloud 教程推荐:https://www.javastack.cn/categories/Spring-Cloud/

No~ 问题来了,前端仍然会报错:“不允许有多个’Access-Control-Allow-Origin’ CORS头”。

Access to XMLHttpRequest at 'http://192.168.2.137:8088/api/two' from origin 'http://localhost:3200' has been blocked by CORS policy:
The 'Access-Control-Allow-Origin' header contains multiple values '*, http://localhost:3200', but only one is allowed.

仔细查看返回的响应头,里面包含了两份Access-Control-Allow-Origin头。

我们用客户端版的PostMan做一个模拟,在请求里设置头:Origin : * ,查看返回结果的头:

不能用Chrome插件版,由于浏览器的限制,插件版设置Origin的Header是无效的

Spring Cloud 轻松解决跨域,别再乱用了!

发现问题了:

VaryAccess-Control-Allow-Origin 两个头重复了两次,其中浏览器对后者有唯一性限制!

分析

Spring Cloud Gateway是基于SpringWebFlux的,所有web请求首先是交给DispatcherHandler进行处理的,将HTTP请求交给具体注册的handler去处理。

我们知道Spring Cloud Gateway进行请求转发,是在配置文件里配置路由信息,一般都是用url predicates模式,对应的就是RoutePredicateHandlerMapping 。所以,DispatcherHandler会把请求交给 RoutePredicateHandlerMapping.

Spring Cloud 轻松解决跨域,别再乱用了!

那么,接下来看下 RoutePredicateHandlerMapping.getHandler(ServerWebExchange exchange) 方法,默认提供者是其父类 AbstractHandlerMapping

@Override
public Mono<Object> getHandler(ServerWebExchange exchange) {
    return getHandlerInternal(exchange).map(handler -> {
        if (logger.isDebugEnabled()) {
            logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
        }
        ServerHttpRequest request = exchange.getRequest();
        // 可以看到是在这一行就进行CORS判断,两个条件:
        // 1. 是否配置了CORS,如果不配的话,默认是返回false的
        // 2. 或者当前请求是OPTIONS请求,且头里包含ORIGIN和ACCESS_CONTROL_REQUEST_METHOD
        if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
            CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(exchange) : null);
            CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);
            config = (config != null ? config.combine(handlerConfig) : handlerConfig);
            //此处交给DefaultCorsProcessor去处理了
            if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
                return REQUEST_HANDLED_HANDLER;
            }
        }
        return handler;
    });
}

注:

网上有些关于修改Gateway的CORS设定的方式,是跟前面SpringBoot一样,实现一个CorsWebFilter的Bean,靠写代码提供 CorsConfiguration ,而不是修改Gateway的配置文件。其实本质,都是将配置交给corsProcessor去处理,殊途同归。但靠配置解决永远比hard code来的优雅。

该方法把Gateway里定义的所有的 GlobalFilter 加载进来,作为handler返回,但在返回前,先进行CORS校验,获取配置后,交给corsProcessor去处理,即DefaultCorsProcessor

看下DefaultCorsProcessor的process方法:

@Override
public boolean process(@Nullable CorsConfiguration config, ServerWebExchange exchange) {

    ServerHttpRequest request = exchange.getRequest();
    ServerHttpResponse response = exchange.getResponse();
    HttpHeaders responseHeaders = response.getHeaders();

    List<String> varyHeaders = responseHeaders.get(HttpHeaders.VARY);
    if (varyHeaders == null) {
        // 第一次进来时,肯定是空,所以加了一次VERY的头,包含ORIGIN, ACCESS_CONTROL_REQUEST_METHOD和ACCESS_CONTROL_REQUEST_HEADERS
        responseHeaders.addAll(HttpHeaders.VARY, VARY_HEADERS);
    }
    else {
        for (String header : VARY_HEADERS) {
            if (!varyHeaders.contains(header)) {
                responseHeaders.add(HttpHeaders.VARY, header);
            }
        }
    }

    if (!CorsUtils.isCorsRequest(request)) {
        return true;
    }

    if (responseHeaders.getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
        logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
        return true;
    }

    boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
    if (config == null) {
        if (preFlightRequest) {
            rejectRequest(response);
            return false;
        }
        else {
            return true;
        }
    }

    return handleInternal(exchange, config, preFlightRequest);
}

// 在这个类里进行实际的CORS校验和处理
protected boolean handleInternal(ServerWebExchange exchange,
                                 CorsConfiguration config, boolean preFlightRequest) {

    ServerHttpRequest request = exchange.getRequest();
    ServerHttpResponse response = exchange.getResponse();
    HttpHeaders responseHeaders = response.getHeaders();

    String requestOrigin = request.getHeaders().getOrigin();
    String allowOrigin = checkOrigin(config, requestOrigin);
    if (allowOrigin == null) {
        logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
        rejectRequest(response);
        return false;
    }

    HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
    List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
    if (allowMethods == null) {
        logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
        rejectRequest(response);
        return false;
    }

    List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
    List<String> allowHeaders = checkHeaders(config, requestHeaders);
    if (preFlightRequest && allowHeaders == null) {
        logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
        rejectRequest(response);
        return false;
    }
    //此处添加了AccessControllAllowOrigin的头
    responseHeaders.setAccessControlAllowOrigin(allowOrigin);

    if (preFlightRequest) {
        responseHeaders.setAccessControlAllowMethods(allowMethods);
    }

    if (preFlightRequest && !allowHeaders.isEmpty()) {
        responseHeaders.setAccessControlAllowHeaders(allowHeaders);
    }

    if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
        responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
    }

    if (Boolean.TRUE.equals(config.getAllowCredentials())) {
        responseHeaders.setAccessControlAllowCredentials(true);
    }

    if (preFlightRequest && config.getMaxAge() != null) {
        responseHeaders.setAccessControlMaxAge(config.getMaxAge());
    }

    return true;
}

可以看到,在DefaultCorsProcessor 中,根据我们在appliation.yml 中的配置,给Response添加了 VaryAccess-Control-Allow-Origin 的头。

Spring Cloud 轻松解决跨域,别再乱用了!

再接下来就是进入各个GlobalFilter进行处理了,其中NettyRoutingFilter 是负责实际将请求转发给后台微服务,并获取Response的,重点看下代码中filter的处理结果的部分:

Spring Cloud 轻松解决跨域,别再乱用了!

其中以下几种header会被过滤掉的:

Spring Cloud 轻松解决跨域,别再乱用了!

很明显,在图里的第3步中,如果后台服务返回的header里有 VaryAccess-Control-Allow-Origin ,这时由于是putAll,没有做任何去重就加进去了,必然会重复,看看DEBUG结果验证一下:

Spring Cloud 轻松解决跨域,别再乱用了!

验证了前面的发现。

解决

解决的方案有两种:

1. 利用 DedupeResponseHeader 配置:

spring:
    cloud:
        gateway:
          globalcors:
            cors-configurations:
              '[/**]':
                allowedOrigins: "*"
                allowedHeaders: "*"
                allowedMethods: "*"
          default-filters:
          - DedupeResponseHeader=Vary Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST

DedupeResponseHeader 加上以后会启用DedupeResponseHeaderGatewayFilterFactory 在其中,dedupe方法可以按照给定策略处理值

private void dedupe(HttpHeaders headers, String name, Strategy strategy) {
  List<String> values = headers.get(name);
  if (values == null || values.size() <= 1) {
   return;
  }
  switch (strategy) {
  // 只保留第一个
  case RETAIN_FIRST:
   headers.set(name, values.get(0));
   break;
  // 保留最后一个
  case RETAIN_LAST:
   headers.set(name, values.get(values.size() - 1));
   break;
  // 去除值相同的
  case RETAIN_UNIQUE:
   headers.put(name, values.stream().distinct().collect(Collectors.toList()));
   break;
  default:
   break;
  }
 }
  • 如果请求中设置的Origin的值与我们自己设置的是同一个,例如生产环境设置的都是自己的域名xxx.com或者开发测试环境设置的都是*(浏览器中是无法设置Origin的值,设置了也不起作用,浏览器默认是当前访问地址),那么可以选用RETAIN_UNIQUE策略,去重后返回到前端。
  • 如果请求中设置的Oringin的值与我们自己设置的不是同一个,RETAIN_UNIQUE策略就无法生效,比如 ”*“ 和 ”xxx.com“是两个不一样的Origin,最终还是会返回两个Access-Control-Allow-Origin 的头。此时,看代码里,response的header里,先加入的是我们自己配置的Access-Control-Allow-Origin的值,所以,我们可以将策略设置为RETAIN_FIRST ,只保留我们自己设置的。

大多数情况下,我们想要返回的是我们自己设置的规则,所以直接使用RETAIN_FIRST 即可。实际上,DedupeResponseHeader 可以针对所有头,做重复的处理。

2. 手动写一个 CorsResponseHeaderFilterGlobalFilter 去修改Response中的头。

@Component
public class CorsResponseHeaderFilter implements GlobalFilter, Ordered {

    private static final Logger logger = LoggerFactory.getLogger(CorsResponseHeaderFilter.class);

    private static final String ANY = "*";

    @Override
    public int getOrder() {
        // 指定此过滤器位于NettyWriteResponseFilter之后
        // 即待处理完响应体后接着处理响应头
        return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
    }

    @Override
    @SuppressWarnings("serial")
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            exchange.getResponse().getHeaders().entrySet().stream()
                    .filter(kv -> (kv.getValue() != null && kv.getValue().size() > 1))
                    .filter(kv -> (kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)
                            || kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)
                            || kv.getKey().equals(HttpHeaders.VARY)))
                    .forEach(kv ->
                    {
                        // Vary只需要去重即可
                        if(kv.getKey().equals(HttpHeaders.VARY))
                            kv.setValue(kv.getValue().stream().distinct().collect(Collectors.toList()));
                        else{
                            List<String> value = new ArrayList<>();
                            if(kv.getValue().contains(ANY)){  //如果包含*,则取*
                                value.add(ANY);
                                kv.setValue(value);
                            }else{
                                value.add(kv.getValue().get(0)); // 否则默认取第一个
                                kv.setValue(value);
                            }
                        }
                    });
        }));
    }
}

此处有两个地方要注意:

1)根据下图可以看到,在取得返回值后,Filter的Order 值越大,越先处理Response,而真正将Response返回到前端的,是 NettyWriteResponseFilter, 我们要想在它之前修改Response,则Order 的值必须比NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER 大。

Spring Cloud 轻松解决跨域,别再乱用了!

2)修改后置filter时,网上有些文字使用的是 Mono.defer去做的,这种做法,会从此filter开始,重新执行一遍它后面的其他filter,一般我们会添加一些认证或鉴权的 GlobalFilter ,就需要在这些filter里用ServerWebExchangeUtils.isAlreadyRouted(exchange) 方法去判断是否重复执行,否则可能会执行二次重复操作,所以建议使用fromRunnable 避免这种情况。

作者:EdisonXu - 徐焱飞
来源:http://edisonxu.com/2020/10/14/spring-cloud-gateway-cors.html

近期热文推荐:

1.1,000+ 道 Java面试题及答案整理(2022最新版)

2.劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4.别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!

5.《Java开发手册(嵩山版)》最新发布,速速下载!

觉得不错,别忘了随手点赞+转发哦!文章来源地址https://www.toymoban.com/news/detail-709739.html

到了这里,关于Spring Cloud 轻松解决跨域,别再乱用了!的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 解决 Axios 跨域问题,轻松实现接口调用

    跨域是指访问另外一个域的资源,由于浏览器的同源策略,默认情况下使用 XMLHttpRequest 和 Fetch 请求时是不允许跨域的。跨域的根本原因是浏览器的同源策略,这是由浏览器对 JavaScript 施加的安全限制。 跨域请求被阻止 (Cross-Origin Request Blocked) : 这是由浏览器实施的同源策略

    2024年02月05日
    浏览(32)
  • 解密Spring Cloud微服务调用:如何轻松获取请求目标方的IP和端口

    公众号「架构成长指南」,专注于生产实践、云原生、分布式系统、大数据技术分享。 目的 Spring Cloud 线上微服务实例都是2个起步,如果出问题后,在没有ELK等日志分析平台,如何确定调用到了目标服务的那个实例,以此来排查问题 效果 可以看到服务有几个实例是上线,并

    2024年02月05日
    浏览(26)
  • 【Spring Cloud】深入探索统一网关 Gateway 的搭建,断言工厂,过滤器工厂,全局过滤器以及跨域问题

    在微服务架构中,网关是至关重要的组件,具有多重职责,为整个系统提供了一系列关键功能。从下面的微服务结构图中,我们可以明确网关的几项主要作用: 微服务结构图: 请求过滤与安全: 用户的所有请求首先经过网关,这使得网关成为系统的第一道防线。通过对传入

    2024年02月07日
    浏览(43)
  • 什么是跨域问题 ?Spring MVC 如何解决跨域问题 ?Spring Boot 如何解决跨域问题 ?

    目录 1. 什么是跨域问题 ? 2. Spring MVC 如何解决跨域问题 ? 3. Spring Boot 如何解决跨域问题 ?  跨域问题指的是不同站点之间,使用 ajax 无法相互调用的问题。 跨域问题的 3 种情况: 1. 协议不同,例如 http 和 https; http://127.0.0.1:8080 https://127.0.0.1:8080 2. 域名不同; 一级域名、

    2024年02月10日
    浏览(35)
  • 别再满屏找日志了!推荐一款 IDEA 日志管理插件,看日志轻松多了!

    Grep Console是一款方便开发者对idea控制台输出日志进行个性化管理的插件。 支持自定义规则来过滤日志信息; 支持不同级别的日志的输出样式的个性化配置; 总结:通过过滤功能、输出日志样式配置功能,可以更方便开发者在大量的日志信息中筛选出自己比较关注的日志信息

    2024年02月07日
    浏览(40)
  • 【Spring security 解决跨域】

    主页传送门:📀 传送   Spring Security是一个功能强大且高度可定制的,主要负责为Java程序提供声明式的身份验证和访问控制的安全框架。其前身是Acegi Security,后来被收纳为Spring的一个子项目,并更名为了Spring Security。Spring Security的底层主要是基于Spring AOP和Servlet过滤器来实

    2024年02月13日
    浏览(30)
  • Spring Security——09,解决跨域

    浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨 域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。 前后端分离项目,前端项目和后端项目一般都不是同源的,所以

    2024年04月15日
    浏览(18)
  • 快速解决Spring Boot跨域困扰:使用CORS实现无缝跨域支持

    什么是跨域? 跨域(Cross-Origin Issue)的存在是因为浏览器的安全限制,它防止恶意网站利用跨域请求来获取用户的敏感信息或执行恶意操作。浏览器通过实施同源策略来限制网页在不同源之间进行资源访问或交互的情况。当一个网页的协议、域名、或端口与当前页面的协议、

    2024年02月12日
    浏览(38)
  • Spring Boot中解决跨域问题(CORS)

    首先解释什么是跨域,跨域就是前端和后端的端口号不同;会产生跨域问题,这里浏览器的保护机制(同源策略)。 同源策略:前端和后端的协议、域名、端口号三者都相同叫做同源。 我们看一下不同源: VUE:http://localhost:8080 Spring: http://localhost:8081/list 当我们出现跨域问题

    2024年02月06日
    浏览(44)
  • 解决Spring Boot跨域问题(配置JAVA类)

    跨域问题指的是不同端口之间,使用 ajax 无法相互调用的问题。跨域问题本质是浏览器的一种保护机制,它是为了保证用户的安全,防止恶意网站窃取数据。 比如前端用的端口号为8081,后端用的端口号为8080,后端想接收前端发送的数据就会出现跨域问题。 如图所示: 这里

    2024年01月17日
    浏览(31)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包