一直听说跨域问题,印象中后端开发就是包个jsonp的接口就好了嘛。研究了一下,发现其实是需要前后端配合解决的,而且现在更好的解决的方案是CORS。看了很多资料,纸上得来终觉浅,绝知此事要躬行。
什么是跨域问题
简单广泛地讲,跨域就是一个域名下请求其他域名的资源。
广义的跨域包括:
1.) 资源跳转: A链接、重定向、表单提交
2.) 资源嵌入:link、script、img、frame 等dom标签,还有样式中background:url()、@font-face()等文件外链
3.) 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等
不过我们通常所说的跨域是指由浏览器同源策略限制的一类请求场景。
我们一般要解决的跨域问题,就是指如何安全正常地通过浏览器在一个域名下调用其他域名的请求服务。
浏览器同源策略限制
同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
同源策略限制以下几种行为:
1.) Cookie、LocalStorage 和 IndexDB 无法读取
2.) DOM 和 Js对象无法获得
3.) AJAX 请求不能发送
复制代码
参考资料
segmentfault.com/a/119000001…
www.ruanyifeng.com/blog/2016/0…
跨域解决方案
针对不同的场景,有很多解决方案。
1、 通过jsonp跨域
2、 document.domain + iframe跨域
3、 location.hash + iframe
4、 window.name + iframe跨域
5、 postMessage跨域
6、 跨域资源共享(CORS)
7、 nginx代理跨域
8、 nodejs中间件代理跨域
9、 WebSocket协议跨域
参考资料
segmentfault.com/a/119000001…
JSONP
对于请求数据服务,一般就是使用jsonp,cors, nginx这几种。
json曾经非常流行,它的基本思想是,网页通过添加一个元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。
JSOP包含两部分:回调函数和数据,回调函数是在响应到来时应该调用的函数,一般通过查询字符串添加;数据就是传入回调函数中的JSON数据,确切的说,是一个JSON对象,可以直接访问。
最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。服务端的代码中在http response中添加callback就好。
缺点也很明显,只能实现GET请求,没有POST;从其他域中加载的代码可能不安全;难以确定JSONP请求是否失败(XHR有error事件),常见做法是使用定时器指定响应的允许时间,超出时间认为响应失败。
CORS
CORS跨源资源分享(Cross-Origin Resource Sharing),与JSONP的使用目的相同,它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。它是后来制定的W3C标准,标准文档链接。大部分浏览器已经实现了,可以支持所有类型的HTTP请求,比JSONP更强大。
协议内容
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
复制代码
(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application_x-www-form-urlencoded、multipart_form-data、text/plain
复制代码
凡是不同时满足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不一样的。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight),服务器对这个增加的请求也需要正常响应。
其他就是浏览器会检查核对request 和 response的header, origin等等字段。
参考资料 跨域资源共享 CORS 详解 - 阮一峰的网络日志
CORS方案实践
跨域问题的解决方案首选cors,毕竟是官方标准,安全性上更有保障。
一次请求的链路大约如下:
js发起请求->浏览器->(nginx服务器)->应用服务器->(nginx服务器)->浏览器->js处理返回
前端怎么做
比较简单,设置对应的http协议参数即可。
其中特别的参数 withCredentials 属性,主要是影响cookie信息。
CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。
Access-Control-Allow-Credentials: true
复制代码
另一方面,开发者必须在AJAX请求中打开withCredentials属性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
复制代码
否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。
但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。
后端-直接修改response
后端的处理,主要是针对请求的来源进行判断,给予正常的响应,填充http协议头对应的内容。所以最直接的方式就是在响应的接口处,增加代码填充。
实例:
测试应用是webx的工程,请求的url crmx.admin.taobao.net/data/jxt/si…
package com.taobao.crmx.admin.web.module.screen.data.jxt.siyao;
import javax.servlet.http.HttpServletResponse;
import com.alibaba.citrus.turbine.Context;
import com.alibaba.citrus.turbine.TurbineRunData;
import com.alibaba.citrus.turbine.util.TurbineUtil;
import com.taobao.crmx.admin.dto.ResultDTO;
import com.taobao.crmx.admin.web.module.screen.BaseScreen;
/**
* @author siyao.hq
* @date 2019/1/11
*/
public class MyPage extends BaseScreen {
public Object execute(Context context) {
TurbineRunData rundata = TurbineUtil.getTurbineRunData(request);
try {
if(rundata.getRequest().getMethod() == "OPTIONS"){
rundata.getResponse().setContentType("text/plain; charset=UTF-8");
// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
rundata.getResponse().addHeader("Access-Control-Allow-Origin", "http://ecrm.daily.taobao.net");
rundata.getResponse().addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE");
// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
rundata.getResponse().addHeader("Access-Control-Allow-Credentials", "true");
// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
rundata.getResponse().addHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
return ResultDTO.buildSuccess("pass!");
}else {
rundata.getResponse().setContentType("text/plain; charset=UTF-8");
// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
rundata.getResponse().addHeader("Access-Control-Allow-Origin", "http://ecrm.daily.taobao.net");
rundata.getResponse().addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
rundata.getResponse().addHeader("Access-Control-Allow-Credentials", "true");
rundata.getResponse().addHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
return ResultDTO.buildSuccess("I am happy, It's ok!");
}
}catch (Exception e){
return ResultDTO.buildError("exception!");
}
}
}
复制代码
因为是在接口处来处理,所以只能针对这一个接口。
后端-filter
再往前一点,则可以配置filter来完成http协议头的处理。
通过Filter可以实现用户在访问某个目标资源之前,对访问的请求和响应进行拦截,实现URL级别的权限访问控制。
实现filter的方式也有多种,主要以实现Filter接口,本次实践中继承了 OncePerRequestFilter(spring中的类),它也实现了Filter接口.
实例:
1.实现http协议头的填充
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* @author siyao.hq
* @date 2019/1/11
*/
public class CorsHandleFilter extends OncePerRequestFilter {
private static final Log logger = LogFactory.getLog(CorsHandleFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
logger.error("execute CorsHandleFilter");
response.setContentType("text/plain; charset=UTF-8");
// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.addHeader("Access-Control-Allow-Origin", "http://ecrm.daily.taobao.net");
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.addHeader("Access-Control-Allow-Credentials", "true");
// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.addHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
filterChain.doFilter(request, response);
}
@Override
public void destroy() {
logger.info("CorsHandleFilter destroy");
}
}
复制代码
2.配置filter , 可按url路径匹配规则实现。
<filter>
<filter-name>corsHandleFilter</filter-name>
<filter-class>com.taobao.crmx.admin.filter.CorsHandleFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>corsHandleFilter</filter-name>
<url-pattern>_data_jxt_siyao_myPageNormal.json</url-pattern>
</filter-mapping>
复制代码
后端-spring mvc:cors
spring 4.2及以上版本支持了cors相关的功能,有多种方式可以使用,可以支持多个域名。
官方文档 spring.io/blog/2015/0…
spring MVC cors跨域实现源码解析 - 出门向左 - 博客园
其中需要注意的是,通过xml方式配置mvc:cors,需要如下配置,4.2以上才支持。
xsi:schemaLocation=“http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd"
复制代码
如果要使用spring默认的CorsFilter, 因为CorsFilter没有无参的构造函数,所以需要通过代理类,注入bean作为构造函数的入参,否则无法正常初始化。而且必须使用spring的ContextLoaderListener。
其中可以自定义CorsProcessor,实现自己的处理逻辑,CorsConfiguration
则可以指定http协议头相关的参数。
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
<filter-name>officialCorsFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>officialCorsFilter</param-value>
</init-param>
</filter>
复制代码
<bean id=“myCorsProcessor” class=“com.taobao.crmx.admin.filter.MyCorsProcessor” />
<bean id=“myCorsFilterConfig” class=“com.taobao.crmx.admin.filter.MyCorsFilterConfig” />
<bean id=“officialCorsFilter” name=“officialCorsFilter” class=“org.springframework.web.filter.CorsFilter” >
<constructor-arg ref=“myCorsFilterConfig”></constructor-arg>
<property name=“corsProcessor” ref=“myCorsProcessor”></property>
</bean>
复制代码
如果是webx的工程,这种方式,就不能用了。webx对spring做了一些改造,不能保障初始化的顺序,会出现bean找不到的错误。也不能新增spring的ContextLoaderListener,因为webx的ContextLoaderListener初始化了root context ,会冲突。
xml配置方式mvc:cors,可以初始化一个filter,但是不会被调用,也就是没有过滤效果,因为不在webx的filter chain中.
spring boot 还可以使用@CrossOrigin注解配置。
后端-三方库
com.thetransactioncompany.cors.CORSFilter
后端-配置nginx
集团应用中一般使用了nginx, 所以也可以在nginx这层做请求处理。
在原来的nginx-proxy.conf 中新增一个server, 如果之前没有配置过nginx-proxy.conf,可以从机器上拷贝默认的nginx-proxy.conf,在dockerfile中添加一行copy 。 添加如下配置,仍然是http协议头的填充,其中OPTIONS主要是处理预检请求,直接返回2系列的响应码即可。实例中只能配置一个origin,但是可以通过其他方式,比如增加if判断请求来源的域名,以及location的url映射规则,来实现不同路径规则的访问控制。
实例:
server {
listen 80 default_server;
# defaul_server 只能有一个,如果已经在其他server上指定了,这里可以删除
server_name crmx.admin.taobao.net;
location / {
if ($request_method = ‘OPTIONS’) {
add_header ‘Access-Control-Allow-Origin’ ‘http://ecrm.daily.taobao.net’;
add_header ‘Access-Control-Allow-Credentials’ ‘true’;
# 按需添加header 就好,一般不需要加这么多
add_header ‘Access-Control-Allow-Headers’ ‘Authorization,application/json,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,X-Requested-With,X-Custom-Header’;
add_header ‘Access-Control-Allow-Methods’ ‘GET,POST,OPTIONS’;
return 204;
}
if ($request_method = ‘POST’) {
add_header ‘Access-Control-Allow-Origin’ ‘http://ecrm.daily.taobao.net’;
add_header ‘Access-Control-Allow-Methods’ ‘GET, POST, OPTIONS’;
add_header ‘Access-Control-Allow-Credentials’ ‘true’;
add_header ‘Access-Control-Allow-Headers’ ‘DNT,application/json,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type’;
}
if ($request_method = ‘GET’) {
add_header ‘Access-Control-Allow-Origin’ ‘http://ecrm.daily.taobao.net’;
add_header ‘Access-Control-Allow-Methods’ ‘GET, POST, OPTIONS’;
add_header ‘Access-Control-Allow-Credentials’ ‘true’;
add_header ‘Access-Control-Allow-Headers’ ‘DNT,application/json,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type’;
}
proxy_pass http://127.0.0.1:7001;
}
}
复制代码
可能问题:
1.感觉配置没有效果,有可能没有进入这个server的处理,而是匹配到其他server了,可以检查一下server_name的配置。
2.浏览器报错如下,是nginx配置和接口处response.addHeader 重复了。
3.