前言
最近做系统整合,涉及到统一认证和跨域问题,总结跨域如下,统一认证放另外博客
跨域总结
针对目前都是前后端分离项目,跨域问题很常见,跨域错误通常403如下
针对跨域解决办法通常是创建filter CorsFilter,允许所有跨域,问题基本都能解决。
/**
* 全局跨域配置
*/
@Configuration
public class GlobalCorsConfig {
/**
* 允许跨域调用的过滤器
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
//允许所有域名进行跨域调用
config.addAllowedOrigin("*");
//允许跨越发送cookie
config.setAllowCredentials(true);
//放行全部原始头信息
config.addAllowedHeader("*");
//允许所有请求方法跨域调用
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
或者在NGINX上配置如下也可以解决
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Credentials 'true';
add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
那么这样解决的背后原理是什么呢?
先说跨域的介绍和历史
跨域概念
CORS全称Cross-Origin Resource Sharing,意为跨域资源共享。当一个资源去访问另一个不同域名或者同域名不同端口的资源时,就会发出跨域请求。如果此时另一个资源不允许其进行跨域资源访问,那么访问的那个资源就会遇到跨域问题。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
跨域的历史
1995年由Netscape提出同源策略,浏览器在发送Ajax请求时,只接收同域服务器响应的数据资源;那么什么才算同域呢?很简单,协议、域名、端口全部相同才算同一域下,三个条件有一个不一致,都不算同域,既跨域;
因此,即使是我们自己的域名服务器,而二级域名或三级域名不一致,也会出现跨域,如:http://a.yhd.com与 http://b.yhd.com 之间需要数据交互,就跨域了;
这就让人很苦恼了,明明都是我自己的域名,我自己的服务器,还是受到了同源策略的保护无法进行数据交互;这不能算是设计缺陷,只能说当年考虑不周全,也有可能因为当年网络通信协议的不健全,原因不得而知;
解决跨域就是够使两个不同域下的数据进行顺利交互就可以了;各路解决方案中,最为典型的有两个:
JSONP和同域代理;
同域代理就是使用Ajax向同域下的后台发送请求,同时携带真实请求的地址及参数,后台接受请求后直接根据地址及参数转发请求,因为后台是可以直接模拟HTTP客户端发送请求的,所以没有跨域问题,而后台接受到响应数据后再原样返回给前端浏览器,从而实现跨域数据交互;
JSONP是利用了 script 标签的 src 属性来实现跨域数据交互的,因为浏览器解析HTML代码时,原生具有src属性的标签,浏览器都赋予其HTTP请求的能力,而且不受跨域限制,使用src发送HTTP请求,服务器直接返回一段JS代码的函数调用,将服务器数据放在函数实参中,前端提前写好响应的函数准备回调,接收数据,实现跨域数据交互;
JSONP和同域代理,本质上并没有解决Ajax跨域的问题,只是绕开这个问题而另辟蹊径实现的跨域数据交互,在数据交互层面上可以看做技术不成熟时的临时解决方案;但是JSONP 和同域代理 使用了很多年,当然跨域问题也存在了很多年,终于有人看不下去了,提出了浏览器与服务器跨域通信的安全性通信策略,它就是我们今天的主角 跨域资源共享(Cross-origin resource sharing),简称 CORS ; 有了它,我们可以安全放心的抛弃 JSONP 和 同域代理了,步入正统的跨域数据交互的殿堂;
跨域的解决方案:cors
CORS 的使用由一系列传输的HTTP头组成,这些HTTP头有两个作用,
1:用于阻止还是允许浏览器向其他域名发起请求;
2:用于接受还是拒绝其他域名返回的响应数据;
因此只要搞清楚什么样的头信息是控制浏览器发送还是不发送请求,什么样的头信息控制浏览器接受还是拒绝服务器的响应数据,这两点搞明白,CORS就算彻底搞清楚了;
跨域请求被分为了两种类型,一种是简单请求,一种是复杂请求 (需预检请求);简单请求与普通的ajax请求无异;但复杂请求,必须在正式发送请求前先发送一个OPTIONS方法的请求已得到服务器的同意,若没有得到服务器的同意,浏览器不会发送正式请求;因此下图跨域错误就是OPTIONS请求没有得到服务器的同意,浏览器不发送正式请求的错误。
我们目前前后端交互content_type都是applicaion/json,这种属于复杂请求,因此我们忽略简单请求,只分析复杂请求。
例子如下:
前后端分离,前端运行在http://localhost:8090,访问后端接口http://localhost:8082/admin/login,因为端口不同,发生跨域,跨域会先发个OPTIONS请求判断服务器是否允许跨域,那么浏览器是如何知道服务器允许跨域呢?服务器会在response header内返回Access-Control-Allow-Origin,浏览器根据这个头判断接受服务器响应。
跨域必须会有http header的Origin字段,表示请求来源,比如下图是个预检(preFlight)请求,如下图
服务端没有返回Access-Control-Allow-Origin,因此浏览器决绝发出跨域的正式请求,报错如下
"预检"请求除了Origin字段,"预检"请求的头信息包括两个特殊字段。
(1)Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求(正式请求)会用到哪些HTTP方法,上例是POST。
(2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是content_type。
在增加了CorsFilter后,请求截图如下:
第一个/admin/login是预检请求OPTIONS,服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,做出回应,在response header内返回了Access-Control-Allow-Origin: http://localhost:8090,因此浏览器认为服务器允许跨域,因此跨域发送正式请求(Access-Control-Allow-Origin字段也可以设为星号,表示同意任意跨源请求。)。
第二个/admin/login是正式请求。
CORS中最常使用的request请求头为 Origin、Access-Control-Request-Headers、Access-Control-Request-Method;
CORS中最常使用的response响应头为 Access-Control-Allow-Origin、Access-Control-Allow-Headers、Access-Control-Expose-Headers;
request上送字段:
Origin:该字段必须,在request内上送,表示请求的来源,在跨域中,表示跨域请求的来源方
Access-Control-Request-Method:该字段必须,用来列出浏览器的CORS请求会用到哪些HTTP方法
Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是content_type
。
response响应字段:
Access-Control-Allow-Origin:该字段必需,请求的资源能共享给哪些域,它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求
Access-Control-Allow-Headers:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
Access-Control-Allow-Credentials:该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
Access-Control-Expose-Headers:该字段可选。CORS请求时,ajax请求XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
Access-Control-Max-Age:该字段可选,用来指定本次预检请求的有效期,单位为秒。在此缓存期间,不用发出另一条预检请求。
CorsFilter简单分析
从上面分析,允许跨域,关键是response header内返回Access-Control-Allow-Origin,这个字段是在CorsFilter内,源码如下
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (CorsUtils.isCorsRequest(request)) {//request header有Origin则认为是跨域请求
CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
if (corsConfiguration != null) {
boolean isValid = this.processor.processRequest(corsConfiguration, request, response);//关键方法
//CorsUtils.isPreFlightRequest(request)方法为true的情况:request header有Origin、Access-Control-Request-Method且http method是OPTIONS,则认为是预检
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
return;//是预检方法,或者跨域不允许,直接返回
}
}
}
filterChain.doFilter(request, response);
}
//在关键方法内,如果允许跨域,handleInternal会给response header设置Access-Control-Allow-Origin,表示允许跨域
nginx设置允许跨域
通常允许跨域,可以简单粗暴使用nginx配置,而不使用CorsFilter,只需要设置如下即可
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Credentials 'true';
add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
原因也是给response header设置Access-Control-Allow-Origin,这样浏览器就允许跨域了,通常许多项目也是这么简单粗暴的设置,或者粗暴的加上CorsFilter。
cors总结
跨域就是ajax请求访问不同域下的资源,由于同源策略控制,默认不允许跨域。跨域是浏览器和服务端共同实现的(http协议),浏览器都支持跨域,关键在服务器,服务器通过Access-Control-Allow-Origin来控制是否允许跨域。Access-Control-Allow-XXX头的作用是浏览器是否可以发起跨域,这个过程和前端代码无关。
一些其它零碎记录
AJAX请求
AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)。
AJAX 不是新的编程语言,而是一种使用现有标准的新方法。
AJAX 最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。
AJAX 不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行。
后端如何判断是AJAX请求和json请求
//判断ajax请求
public static boolean isRequestAjax(HttpServletRequest request) {
String ajaxHeader = request.getHeader("X-Requested-With");
boolean isAjax = "XMLHttpRequest".equals(ajaxHeader);
return isAjax;
}
//判断json请求
public static boolean isJsonRequest(HttpServletRequest request) {
return request.getHeader("Accept") != null
&& request.getHeader("Accept").contains("application/json");
}
在服务器端判断request来自Ajax请求(异步)还是传统请求(同步):
两种请求在请求的Header不同,Ajax 异步请求比传统的同步请求多了一个头参数x-requested-with ,可以利用它,request.getHeader("x-requested-with"); 为 null,则为传统同步请求,为 XMLHttpRequest,则为 Ajax 异步请求。
AJAX请求和json请求区别
目前来说,前后端交互,数据类型都是application/json,数据类型是json也实际使用的是ajax请求,单纯使用ajax时候accept content_type是form表单。
ajax和axios请求json数据
目前前后端交互,通常使用的vue和react都是通过axios和后端发生交互,axios是通过promise实现对ajax技术的一种封装,就像jQuery实现ajax封装一样。
简单来说: ajax技术实现了网页的局部数据刷新,axios实现了对ajax的封装。axios是ajax 但ajax不止axios
跨域判断
//请求header有Origin则判断为跨域
public static boolean isCorsRequest(HttpServletRequest request) {
return (request.getHeader("Origin") != null);
}
http header内的referer的作用
referer 是 HTTP 请求header 的一部分,当浏览器(或者模拟浏览器行为)向web 服务器发送请求的时候,头信息里有包含Referer。比如我在www.google.com 里有一个www.baidu.com 链接,那么点击这个www.baidu.com ,它的header 信息里就有:Referer=http://www.google.com 由此可以看出来吧。它就是表示一个来源
Referer的作用?
1.防盗链。
我在www.google.com里有一个www.baidu.com链接,那么点击这个www.baidu.com,它的header信息里就有:
Referer=http://www.google.com
那么可以利用这个来防止盗链了,比如我只允许我自己的网站访问我自己的图片服务器,那我的域名是www.google.com,那么图片服务器每次取到Referer来判断一下是不是我自己的域名www.google.com,如果是就继续访问,不是就拦截。
这是不是就达到防盗链的效果了?
将这个http请求发给服务器后,如果服务器要求必须是某个地址或者某几个地址才能访问,而你发送的referer不符合他的要求,就会拦截或者跳转到他要求的地址,然后再通过这个地址进行访问。
2.防止恶意请求。
比如静态请求是*.html结尾的,动态请求是*.shtml,那么由此可以这么用,所有的*.shtml请求,必须 Referer 为我自己的网站。
Referer=http://www.google.com
空Referer是怎么回事?什么情况下会出现Referer?
首先,我们对空 Referer 的定义为, Referer 头部的内容为空,或者,一个 HTTP 请求中根本不包含 Referer 头部。
那么什么时候 HTTP 请求会不包含 Referer 字段呢?根据Referer的定义,它的作用是指示一个请求是从哪里链接过来,那么当一个请求并不是由链接触发产生的,那么自然也就不需要指定这个请求的链接来源。
比如,直接在浏览器的地址栏中输入一个资源的URL地址,那么这种请求是不会包含 Referer 字段的,因为这是一个“凭空产生”的 HTTP 请求,并不是从一个地方链接过去的。
那么在防盗链设置中,允许空Referer和不允许空Referer有什么区别?
允许 Referer 为空,意味着你允许比如浏览器直接访问,就是空。