什么是跨域
跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,使用XMLHttpRequest进行跨域请求会被浏览器拦截。
由于浏览器同源策略,凡是发送请求url的协议、域名、端口三者之间任意一与当前页面地址不同即为跨域。
跨域解决方案
跨域资源共享(CORS)
CORS是主流的跨域解决方案,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)。
跨域请求默认不携带cookie,这时只需要服务端设置Access-Control-Allow-Origin,如果需要请求携带cookie(由于同源策略的限制,携带的不是当前页的cookie而是跨域请求接口所在域的cookie),则前后端都需要设置。
const xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容
// 前端设置是否带cookie
xhr.withCredentials = true;
xhr.open("post", "http://www.demo2.com:8080/login", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("user=admin");
xhr.onreadystatechange = () => {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
};
HTTP请求可以分为简单请求和复杂请求,CORS这两类请求按不同的策略进行跨域资源共享协商。
当HTTP请求出现以下两种情况时,浏览器认为是简单请求:
- 请求方法是GET、HEAD或者POST,并且当请求方法是POST时,Content-Type必须是application/x-www-form-urlencoded, multipart/form-data或着text/plain中的一个值。
- 请求中没有自定义HTTP头部。
对于简单跨域请求,浏览器要做的就是在HTTP请求中添加Origin Header,将JavaScript脚本所在域填充进去,向其他域的服务器请求资源。服务器端收到一个简单跨域请求后,根据资源权限配置,在响应头中添加Access-Control-Allow-Origin Header。浏览器收到响应后,查看Access-Control-Allow-Origin Header,如果当前域已经得到授权,则将结果返回给JavaScript。否则浏览器忽略此次响应。
对于复杂跨域请求浏览器在发送真实HTTP请求之前会先发送一个OPTIONS的预检请求,检测服务器端是否支持真实请求进行跨域资源访问,真实请求的信息在OPTIONS请求中通过Access-Control-Request-Method Header和Access-Control-Request-Headers Header描述,此外与简单跨域请求一样,浏览器也会添加Origin Header。
服务器端接到预检请求后,根据资源权限配置,在响应头中放入Access-Control-Allow-Origin Header、Access-Control-Allow-Methods和Access-Control-Allow-Headers Header,分别表示允许跨域资源请求的域、请求方法和请求头。
此外,服务器端还可以加入Access-Control-Max-Age响应头,允许浏览器在指定时间内,无需再发送预检请求进行协商,直接用本次协商结果即可。浏览器根据OPTIONS请求返回的结果来决定是否继续发送真实的请求进行跨域资源访问。
@WebFilter("/*")
public class CORSFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
// 告诉浏览器允许所有的域访问
// 注意 * 不能满足带有cookie的访问,Origin 必须是全匹配
// resp.addHeader("Access-Control-Allow-Origin", "*");
// 解决办法通过获取Origin请求头来动态设置
String origin = request.getHeader("Origin");
if (StringUtils.hasText(origin)) {
resp.addHeader("Access-Control-Allow-Origin", origin);
}
// 允许带有cookie访问
resp.addHeader("Access-Control-Allow-Credentials", "true");
// 告诉浏览器允许跨域访问的方法
resp.addHeader("Access-Control-Allow-Methods", "*");
// 告诉浏览器允许带有Content-Type,header1,header2头的请求访问
// resp.addHeader("Access-Control-Allow-Headers", "Content-Type,header1,header2");
// 设置支持所有的自定义请求头
String headers = request.getHeader("Access-Control-Request-Headers");
if (StringUtils.hasText(headers)) {
resp.addHeader("Access-Control-Allow-Headers", headers);
}
// 告诉浏览器缓存OPTIONS预检请求1小时,避免非简单请求每次发送预检请求,提升性能
resp.addHeader("Access-Control-Max-Age", "3600");
chain.doFilter(request, resp);
}
}
JSONP
拥有src属性和href属性的
const script = document.createElement("script");
script.type = "text/javascript";
// 传参并指定回调执行函数为onBack
script.src = "http://www.demo2.com:8080/login?user=admin&callback=onBack";
document.head.appendChild(script);
// 回调执行函数
function onBack(res) {
console.log(JSON.stringify(res));
}
// 服务端返回如下(返回时即执行全局函数):
// onBack({"status": true, "user": "admin"})
代理跨域
同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨域问题。
通过代理服务器(域名与demo1相同,端口不同)做跳板机,反向代理访问demo2接口,还可以修改cookie中demo信息,方便当前域cookie写入。
postMessage跨域
使用postMessage跨域很简单,通过postMessageAPI对目标窗口发送数据,通过监听message事件接收数据,从而实现跨窗口的异域通讯。
WebSocket跨域
websocket协议没有同源策略的限制,而且它本身就有意被设计成可以跨域的一个手段。由于历史原因,跨域检测一直是由浏览器端来做,但是WebSocket出现以后,对于WebSocket的跨域检测工作就交给了服务端,浏览器仍然会带上一个Origin跨域请求头,服务端则根据这个请求头判断此次跨域WebSocket请求是否合法。
iframe跨域
document.domain + iframe跨域
如果主域相同只有子域不一样时,通过设置两个页面的document.domain都为主域名就实现了同域。
location.hash + iframe跨域
location.hash属性是一个可读可写的字符串,该字符串是URL的锚部分(从# 号开始的部分)。 通过location.hash跨域就是利用动态改变location.hash不会重载iframe的特性将要传递的数据放在url里,因此传递数据量有限、安全性也比较低。
window.name + iframe跨域
name是window对象的一个属性,当在iframe中加载新页面时,window.name不会发生变化。
给iframe赋值跨域的链接,加载完后触发onload事件,此时iframe已经拿到数据放在window.name里,因为浏览器同源策略没法直接拿iframe的name值,所以将iframe的地址改成同域名下的一个网页,在用contentWindow方法获取iframe的name值获取数据。
a.html:
let flag = false;
const iframe = document.createElement("iframe");
// 加载跨域页面
iframe.src = "http://www.demo2.com/b.html";
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = () => {
if (flag) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
console.log(iframe.contentWindow.name);
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
iframe.contentWindow.document.write("");
iframe.contentWindow.close();
document.body.removeChild(iframe);
return;
}
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = "http://www.demo1.com/proxy.html";
flag = true;
};
document.body.appendChild(iframe);
b.html:
window.name = "This is demo2 data!";
window.name值最大支持2M,如果给window.name赋值,window.name会调用类似toString的方法将赋给它的值转换成对应的字符串表示。