getway网关跨域问题记录
一.问题产生环境
1.1 为什么会产生跨域问题?
跨域不一定都会有跨域题。
因为跨域问题是浏览器对于ajax请求的一种安全限制;
一个页面发起的 ajax请求,只能是与当前页域名相同的路径,这能有效的阻止跨站攻击;
因此:
跨域问题是针对ajax的一种限制但是这却给我们的开发帯来了不便,而且在实际生产环境中,
肯定会有很多台服务器之间交互,地址和端口都可能不同,怎么办?
1.2 因为公司微服务项目是前后端分离,前后分离后采用了SpringCloud Getway网关技术,这样请求会经历三个流程:
1.前端请求 ===》2.网关 ==》3.后端接口
1.这是单独项目的前段代码:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Insert title here</title> </head> <!--JQuery在线引用--> <script src="http://code.jquery.com/jquery-1.10.1.min.js"></script> <script> function form_btn(){ var list = JSON.stringify([ {phone: "17320427943", region_code: "620100"}, {phone: "17320427943", region_code: "620100"} ]); alert(list); $.ajax({ url: "http://localhost:8083/p1upgrade/app/api/delPhoneWarnConf?token=1_grand", data: list, type: "POST", contentType: "application/json;charset=utf-8", success: function(result){ var list = eval(result);//解析json alert(list.data); } }); } </script> <body> <h1>springboot访问第一个html页面</h1> <button onclick="form_btn()">提交</button> </body> </html>
2.网关部分的代码:application.yml
server: port: 8083 spring: cloud: gateway: routes: - id: json_route uri: http://192.168.1.206:8080 predicates: - Header=Accept, .*json.* - id: binary_route1 uri: http://192.168.1.206:8080 predicates: - Path=/p1upgrade/app/api/** - id: userCheck121 uri: http://192.168.1.206:8080 predicates: - Path=/p1upgrade/sysManage/userCheck121 - id: appPicManage uri: http://192.168.1.206:8080 predicates: - Path=/p1upgrade/picManage/** - id: path_route uri: http://192.168.1.206:8080 predicates: - Path=/**
3.后端接口部分代码:只是问题重现,接口部分代码没有优化
@RequestMapping("delPhoneWarnConf") public void delPhoneWarnConf(HttpServletResponse rps, @RequestBody List<Map<String,Object>> list) { rps.setContentType("application/json;charset=UTF-8"); List<Map<String, Object>> maps=list; Integer rtcount=0; for (Object object : maps) { Map <String,Object> ret = (Map<String, Object>) object;//取出list里面的值转为map rtcount+=alarmConfigService.delPhoneWarnConf(ret.get("phone").toString(),ret.get("region_code").toString()); } JSONObject result=SystemUtils.responseBody(0, "success", rtcount); try { rps.getWriter().write(result.toString()); } catch (IOException e) { e.printStackTrace(); } }
二.错误现象
三.解决办法
目前常用的跨域解决方案有三种:
(1) jsonp:
最早的解决方案,利用script标签可以跨域的原理实现
缺点:需要服务的支持,
只能发起get请求
(2)nginx反向代理:
思路是利用nginx把跨域反向代理为不跨域,支持各种请求方式
缺点:需要nginx进行额外配置,语义不清晰
(3)CORS:
思路:规范化的跨域请求方案,安全可靠
优势:在服务端进行控制是否允许跨域,靠自定义规则。支持各种请求方式。
缺点:会产生额外的请求
这里一般会采用第三种CORS的跨域方案;
四.什么是CORS
CORS是一个w3c标准,全称是"跨域资源共享"(Cross-origin resource sharing),
但一个请求url的协议,域名,端口三者之间任意与当前页面地址不同即为跨域.它允许阅览器向跨源服务器发送XMLHttpRequest请求,从而克服AJAX只能同源使用的限制.
CORS需要浏览需和服务器同时支持。
浏览器端:
目前,所有浏览器都支持该功能,浏览器不能低于IE10(E10以下不行)。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。
服务端:
CORS通信与AAX没有任何差別,因此你不需要改变以前的业务逻辑。只不过,浏览器会在请求中携带一些头信息,我们需要以此判断是否允许其域,然后在响应头中加入一些信息即可。这一般通过过滤器完成即可
五.原理
非简单请求的CORS请求,会在正式通信前进行一次Http查询请求,又称预检请求。
浏览器先请求服务器,当前网页所在域名是否在服务器许可名单中以及可以使用那些HTTP动词和头信息字段,当客户端得到肯定答复时,浏览器才会正式发出XMLHttpRequest请求。否则就会报错。
“预检”请求的样板:
OPTIONS /cors HTTP/1.1
Access-Control-Request-Headers: content-type Access-Control-Request-Method: POST Origin: http://localhost:8082 Referer: http://localhost:8082/hello User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36
Origin :指出当前请求属于哪个域(协议+域名+端口)。服务会根据这个值决定是否允许其跨域
Access-Control-Request-Method: 接下来会用到的请求方式get,post,put等
Access-Control-Request-Headers:会额外用到的头信息
User-Agent: 浏览器代理
“预检”请求的响应:
六.实现网关跨域请求
虽然原理比较复杂,但是操作起来没那么难:
- 浏览器端都由浏览器自动完成,我们无需操心
- 服务端可以通过拦截器统一实现,不必每次都去进行跨域判定的编写
事实上,Spring已经帮我们写好了CORS的跨域过滤器,内部已经实现了判定逻辑。
- spring-webmvc:CorsFilter
- spring-webflux:CorsWebFilter
springcloud_gateway集成的是webflux,所以这里使用的是CorsWebFilter
6.1 在启动类中编写一个配置类,并且注册CorsWebFilter
@Bean public CorsWebFilter corsWebFilter() { //cors跨域对象 CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("http://localhost:8082");// #允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin config.setAllowCredentials(true); // 允许cookies跨域 config.addAllowedMethod("*");// 允许提交请求的方法,*表示全部允许 config.addAllowedHeader("*");// #允许访问的头信息,*表示全部 config.setMaxAge(18000L);// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了 //cors过滤对象 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); }
6.2 CorsWebFilter实现的是WebFilter,所以也可以在启动类中编写一个配置类,并且注册WebFilter,代码如下,
1 @Bean 2 public WebFilter corsFilter() { 3 return new WebFilter() { 4 5 @Override 6 public Mono<Void> filter(ServerWebExchange ctx, WebFilterChain chain) { 7 ServerHttpRequest request = ctx.getRequest(); 8 if (CorsUtils.isCorsRequest(request)) { 9 ServerHttpResponse response = ctx.getResponse(); 10 HttpHeaders headers = response.getHeaders(); 11 headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); 12 headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,"*"); 13 headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,"*"); 14 headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); 15 headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*"); 16 headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600"); 17 if (request.getMethod() == HttpMethod.OPTIONS) { 18 response.setStatusCode(HttpStatus.OK); 19 return Mono.empty(); 20 } 21 } 22 return chain.filter(ctx); 23 } 24 25 }; 26 }
以上6.1和6.2两种方案经过测试都是可以的,任意采用一种即可,建议采用6.1代码相对简单点,
6.3 效果展示
七.因为网关和后端项目都有配置跨域配置,会导致另一种报错
7.1报错现象
响应头信息:
7.2 接口返回状态已经是200,说明已经访问成功了,但是因为项目后端启动之前也配置过跨域配置,所以会与网关配置冲突,导致响应头中返回了两个可接受的域,正常一般是一个,导致报错还存在,
后端启动类跨域配置,主要是对主接口跨越进行控制,
//springcloud-前端跨域问题的解决方案全局配置 @Bean public WebMvcConfigurerAdapter corsConfigurer() { return new WebMvcConfigurerAdapter() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/anzhou/*").allowCredentials(true).allowedMethods("*").maxAge(3600).allowedHeaders("*"); registry.addMapping("/sysManage/*").allowedOrigins("*").allowCredentials(true).allowedMethods("*").maxAge(3600).allowedHeaders("*"); registry.addMapping("/sysManage/*").allowedOrigins("*").allowCredentials(true).allowedMethods("*").maxAge(3600).allowedHeaders("*"); registry.addMapping("/polluSource/*").allowedOrigins("*").allowCredentials(true).allowedMethods("*").maxAge(3600).allowedHeaders("*"); registry.addMapping("/app/api/*").allowedOrigins("*").allowCredentials(true).allowedMethods("*").maxAge(3600).allowedHeaders("*"); registry.addMapping("/checkManagement/*").allowCredentials(true).allowedMethods("*").maxAge(3600).allowedHeaders("*"); registry.addMapping("/minisite/*").allowedOrigins("*").allowCredentials(true).allowedMethods("*").maxAge(3600).allowedHeaders("*"); registry.addMapping("/**").allowedOrigins("*").allowCredentials(true).allowedMethods("*").maxAge(3600).allowedHeaders("*"); } }; }
所以,应该原因应该是冲突了,但是网关不配置跨域那么请求在网关就被拦截了,根本不会访问到后台接口,所以最后,最好的解决办法就是把跨域配置全都配置在网关中,由网关统一管理,