上一篇博文就讲到了我的处理cors跨域,分享了关键代码。原springBoot版本2.2.2.RELEASE、springCloud版本Hoxton.SR1,那是年前的最新版本,现在项升级到当前与时俱进,于是问题就来了,升级后eureka注册中心、配置中心、各服务、网关都正常启动,就是上到外网环境就会出现跨域问题,昨天本来是听说使用undertow容器比tomcat、jetty性能都强劲,高并发推荐使用。不过我在想为啥既然这么优秀,spring为啥不用还用tomcat,我想应该是undertow没出几年,又没有充值,呵呵。

一、上新版解决跨域关键代码

配置类:
import com.fillersmart.tgsaas.cloud.gateway.filter.CorsResponseHeaderFilter;
import com.fillersmart.tgsaas.data.core.RedisUtil;
import org.springframework.beans.factory.annotation.Value;
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.DefaultCorsProcessor;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.pattern.PathPatternParser;

import javax.annotation.Resource;

/**
 * 解决跨域的配置类
 * @author zhengwen
 **/
@Configuration
public class MyCorsConfiguration {

    /**
     * 配置文件里配置的免校验的请求
     */
    @Value("${web.pass.url}")
    private String webPassUrl;

    /**
     * token的密匙
     */
    @Value("${token.auth.key}")
    private String tokenKey;

    /**
     * token的有效期
     */
    @Value("${token.auth.valid.duration}")
    private String tokenValidDuration;

    @Resource
    RedisUtil redisUtil;

    private static final String ALL = "*";
    private static final Long MAX_AGE = 18000L;

    @Bean
    public CorsResponseHeaderFilter corsResponseHeaderFilter() {
        CorsResponseHeaderFilter corsResponseHeaderFilter = new CorsResponseHeaderFilter(webPassUrl,tokenKey,tokenValidDuration,redisUtil);
        return corsResponseHeaderFilter;
    }

    @Bean
    public CorsWebFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", buildCorsConfiguration());

        CorsWebFilter corsWebFilter = new CorsWebFilter(source, new DefaultCorsProcessor() {
            @Override
            protected boolean handleInternal(ServerWebExchange exchange, CorsConfiguration config,
                                             boolean preFlightRequest)
            {
                // 预留扩展点
                // if (exchange.getRequest().getMethod() == HttpMethod.OPTIONS) {
                return super.handleInternal(exchange, config, preFlightRequest);
                // }

                // return true;
            }
        });

        return corsWebFilter;
    }

    /**
     * 扩展cors配置
     * @return cors配置
     */
    private CorsConfiguration buildCorsConfiguration() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允许cookies跨域
        corsConfiguration.addAllowedOrigin(ALL);
        //允许的方法类型
        corsConfiguration.addAllowedMethod(ALL);
        // #允许访问的头信息,*表示全部
        corsConfiguration.addAllowedHeader(ALL);
        //配置前端js允许访问的自定义响应头
        corsConfiguration.addExposedHeader("Token");
        // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
        corsConfiguration.setMaxAge(MAX_AGE);
        //允许缓存
        corsConfiguration.setAllowCredentials(true);
        return corsConfiguration;
    }

}

PS:这个配置类的关键点是CorsResponseHeaderFilter,这个bean的引入,这个是自己实现的一个过滤器,业务的差异化处理也再这个里面。另外这里要说下corsConfiguration.addExposedHeader("Token");这个不能设置为*,否则会报'*' is not a valid exposed header value,为什么呢?你跟进去看这个方法:

应该是为了安全考虑,另外这里再跟大家扯下,为啥可以head、allowedMethods、resolvedMethods、origins等都可以setList,,为啥设置allowedMethods为*了,resolvedMethods无效?上源码图:

不用我再多言了吧,其他也又setList的方法。

自定义的corsFilter:

package com.fillersmart.tgsaas.cloud.gateway.filter;

import com.alibaba.fastjson.JSON;
import com.fillersmart.tgsaas.data.common.api.ResponseCodeI18n;
import com.fillersmart.tgsaas.data.constant.Constant;
import com.fillersmart.tgsaas.data.core.RedisUtil;
import com.fillersmart.tgsaas.data.core.Result;
import com.fillersmart.tgsaas.data.core.ResultGenerator;
import com.fillersmart.tgsaas.data.util.DateUtil;
import com.fillersmart.tgsaas.data.util.EncryptUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;

/**
 * 跨域请求头处理过滤器扩展
 * Spring Cloud Gateway有bug,所以处理跨域的这个Filter有点特殊
 * bug:会重复设置请求头
 * @author zhengwen
 */
@Slf4j
public class CorsResponseHeaderFilter implements GlobalFilter, Ordered {

    /**
     * 不校验token的请求
     */
    private String webPassUrl;

    /**
     * token的密匙
     */
    private String tokenKey;

    /**
     * token失效时间
     */
    private String tokenValidDuration;

    /**
     * redis对象
     */
    private RedisUtil redisUtil;

    

    private static final String ALL = "*";
    private static final String MAX_AGE = "18000";

    /**
     * 免校验的请求
     */
    private Set<String> allowUrlSet = new HashSet<>();

    /**
     * 构造方法
     * @param webPassUrl 放行的请求
     * @param tokenKey token的密匙
     * @param tokenValidDuration token的有效期
     * @param redisUtil redis对象
     */
    public CorsResponseHeaderFilter(String webPassUrl,String tokenKey,String tokenValidDuration,RedisUtil redisUtil){
        this.webPassUrl = webPassUrl;
        this.tokenKey = tokenKey;
        this.tokenValidDuration = tokenValidDuration;
        this.redisUtil = redisUtil;
    }

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

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        //自定义逻辑
        //这里写自定义的业务路径,比如是否免token校验方法、token合法校验等,因为这些涉及到公司业务,所以不能分享给大家,这些方法大家就自己实现吧,无权限的返回信息类,下面还是跟大家留着。大家业务校验完成了就可以调用return即可。上面的构造函数给配置类调用,同时把配置文件的参数通过spring读取,这个类时没有交给spring管理的,可以看到上面时没有任何spring的注解标签的,只有一个lombok的log注解。
//下面这个链式才是关键,大致意思跟大家解释下,就是取到头文件,遇到orgin、允许缓存的,只取第一个。升级到高版本就提示多头文件请求的跨域信息,或者js里看请求的head没有orgin等信息,黄色感叹号大致意思是使用了临时消息头,请求不会发到后台
        return chain.filter(exchange.mutate().response(decoratedResponse).build()).then(Mono.defer(() -> {
            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)))
                    .forEach(kv ->
                    {
                        kv.setValue(new ArrayList<String>() {{add(kv.getValue().get(0));}});
                    });

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


    /**
     * 无权请求返回结果
     * @param response resp对象
     * @param exchange webExchange对象
     * @return Mono
     */
    private Mono<Void> unAuthResult(ServerHttpResponse response, ServerWebExchange exchange) {
        log.info("---设置无权请求的返回结果--");

        //这行很只要,没有这行,浏览器拒绝讲结果返回给用户
        response.setStatusCode(HttpStatus.OK);

        //不设置response的header,实际是请求已经成功了,给浏览器的假象就是跨域
        HttpHeaders responseHeaders = response.getHeaders();
        responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, ALL);
        responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, ALL);
        responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.TRUE.toString());
        responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, ALL);
        responseHeaders.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, ALL);
        responseHeaders.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);

        //设置返回结果
        Result result = ResultGenerator.genFailResult(ResponseCodeI18n.UNAUTHORIZED.getMsg());

        //转为json字符串
        String resultStr = JSON.toJSONString(result);
        byte[] bytes = resultStr.getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bytes);
        return response.writeWith(Flux.just(buffer));
    }

}

二、为什么失效跟报信息头错误

首先gateway2.0之后使用的不是webmvc,而是webflux,所以pom要引入webflux支持

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
        </exclusion>
    </exclusions>
</dependency>

然后为什么会请求信息头报重复信息头,这里先说下gateway是用到的netty,怎么一步步找到,这里就不说了,大家可以在下面引入的jar找到gateway,进去看源码,我使用的是intellij idea2020.1.1最新版。

源码对于head处理是重复了设置了,已标红,感兴趣的可以看看。

三、跨域测试页面


<html lang="en">
<head>

    <meta charset="UTF-8">
    <meta http-equiv="Access-Control-Allow-Origin" content="*">
    <title>Title</title>

</head>

<link type="test/css" href="css/style.css" rel="stylesheet">

<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>  

<script type="text/javascript">

    $(function(){
        //jQuery.support.cors = true;

    $("#cors").click(

        function(){

            $.ajax({
                //type: 'POST',
                type: 'GET',
                headers:{"Token":"F7429753284D5A047DF012DF1443A351AE33A8FADAA9FC39","Content-Type":"application/json;charset=UTF-8"},
                //url:"http://xxx.xxx.xx.x:8800/commonweb/company/org/info/orgList",
                url:"http://xxx.xx.xx.xx:8800/custweb/user/info/userDetail/1",
                //data:{  "companyOrgInfo": {    "orgName": "2",    "useStatus": 1  },  "page": 1,  "size": 10},
                success:function(data){
                    console.log("success");
                    console.log(data);
                    alert(data);

                }

            })

        });

    });

</script>

<body>

    <input type="button" id="cors" value="cros跨域测试"

</body>

</html>

PS:注意谷歌浏览器F12伺候,看console、network的请求的head等。

三、总结

上面基本上讲清楚怎么处理了,其实我是昨晚9点多发到外网测试没有跨域报错了。但是大家看到了,我的博文今天才写,昨天应该说从下午就开始折腾,换容器、解决升级版本的跨域。同时也要跟大家说,看源码真的很重要,尤其是在用的时候不是那么回事的时候。

 

 

 

 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐