序
上一篇博文就讲到了我的处理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 SetallowUrlSet = 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 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 () {{add(kv.getValue().get(0));}}); }); return chain.filter(exchange); })); } /** * 无权请求返回结果 * @param response resp对象 * @param exchange webExchange对象 * @return Mono */ private Mono 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支持
org.springframework.boot spring-boot-starter-webflux org.springframework spring-webmvc
然后为什么会请求信息头报重复信息头,这里先说下gateway是用到的netty,怎么一步步找到,这里就不说了,大家可以在下面引入的jar找到gateway,进去看源码,我使用的是intellij idea2020.1.1最新版。
源码对于head处理是重复了设置了,已标红,感兴趣的可以看看。
三、跨域测试页面
$(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);
}
})
});
});
PS:注意谷歌浏览器F12伺候,看console、network的请求的head等。
三、总结
上面基本上讲清楚怎么处理了,其实我是昨晚9点多发到外网测试没有跨域报错了。但是大家看到了,我的博文今天才写,昨天应该说从下午就开始折腾,换容器、解决升级版本的跨域。同时也要跟大家说,看源码真的很重要,尤其是在用的时候不是那么回事的时候。