目录
一、什么是跨域
- 当前发起请求的域与该请求指向的资源所在的域不一样,就是跨域
- 若协议 + 域名 + 端口号均相同,那么就是同域,否则是跨域
- localhost和127.0.0.1虽然都指向本机,但也属于跨域
二、ajax跨域请求失败示例
本地测试,编写前后端分离代码,在前端使用ajax请求后端接口
- 后端接口 url
http://127.0.0.1:8080/test/get
- 前端请求 url
http://127.0.0.1:8081
- 浏览器报错信息如下
三、产生跨域问题的原因
1、浏览器的限制
- 跨域问题来源于JavaScript的同源策略。
- 同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
- 当浏览器发现是跨域请求时,就会进行一些校验,当校验不通过,就会报跨域安全问题。
- 上面那个请求失败的示例,F12查看,发现请求返回200 ,虽然浏览器报错,但请求是成功了的
- 可以看出,服务器后台没有进行限制的,是浏览器限制报错。
2、跨域
- 当前发起请求的域与该请求指向的资源所在的域不一样,就会跨域失败。
- 如果是同域请求,是不存在这个问题的。
3、XHR(XMLHttpRequest)请求
- 跨域问题是针对JS和ajax的,html本身没有跨域问题,凡是拥有”src”这个属性的标签都拥有跨域的能力,比如
<script>
、<img>
、<iframe>
,可以直接跨域发送数据并接收数据等
上面3个问题同时满足,才可能产生跨域问题
四、解决方案
1、解除 “浏览器的限制”
- 通过命令行启动浏览器,指定参数,禁止浏览器检查
chrome --disable-web-security
- 指定这个参数,会降低浏览器安全
2、使用JSONP请求
(1) JSONP基本原理
- JSONP(JSON with Padding) 是 json 的一种"使用模式",为了便于客户端使用数据,逐渐形成了一种非正式传输协议
- JSONP的基本思想是,网页通过添加一个
<script>
元素,通过src属性去触发对指定地址的请求,这种做法不受同源政策限制 - 允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了
(2) JSONP调用实现
- 假设接口
http://127.0.0.1:8080/test/get
响应为:{"data": "get ok"}
, 现在需要支持jsonp,怎么办? - 只需要请求时, 增加一个参数 callback, 例如
http://127.0.0.1:8080/test/get?callback=abc
,那么返回到客户端的响应将是:abc({"data": "get ok"})
- 不仅客户端需要修改,服务端也需要修改
- 服务端添加切面
@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
public JsonpAdvice() {
super("callback"); // 与前端传的jsonp参数呼应
}
}
- 客户端jsonp请求
$.ajax({
url : 'http://127.0.0.1:8080/test/get',
datatype : 'jsonp',
jsonp: 'callback',
success: function(data) {
console.log(data);
}
});
- JSONP请求后通过动态生成
<script>
脚本,为src属性指定一个跨域URL(这个导致它只能支持GET请求),生成的<script>
元素如下
<script async src="http://127.0.0.1:8080/test/get?callback=jQuery111302177301635589095_1582778887373&_=1582778887374"></script>
(3) 普通ajax请求与JSONP请求区别
- 普通的ajax请求type为
xhr
,而jsonp请求type为script
- 普通的ajax请求 Content-Type 为
application/json
- jsonp请求 Content-Type 为
application/javascript
- jsonp请求支持GET,不支持POST
3、CORS(跨域资源共享,Cross-Origin Resource Sharing)
- 跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。
- 允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行
(1) 简单请求与非简单请求
- 满足以下条件的请求为简单请求,其余为非简单请求
- 请求方法:GET、HEAD、POST
- 请求头里:
- 无自定义头
- Content-Type 的值仅限于下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
- 预检请求
- 非简单请求首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求
- "预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响
(2) CORS 相关头部信息
1、Origin
- 请求头信息,浏览器携带的,用于表明该跨域请求的请求来源
Origin: http://127.0.0.1:8080
2、Access-Control-Allow-Origin
- 响应头信息,由服务端返回,用于明确指定那些客户端的域名允许访问这个资源
- 它的值可以是一个具体的域名(如 http://127.0.0.1:8081)也可以是
*
表示任意域名
Access-Control-Allow-Origin: * //该资源可以被任意外域访问
Access-Control-Allow-Origin: http://127.0.0.1:8080
3、Access-Control-Request-Method 和 Access-Control-Request-Headers
- 请求头信息,在预检请求中携带了这两个头部字段
Access-Control-Request-Method: POST
告知服务器,实际请求将使用 POST 方法Access-Control-Request-Headers: X-PINGOTHER, Content-Type
告知服务器,实际请求将携带两个自定义请求头部字段:X-PINGOTHER 与 Content-Type
4、Access-Control-Allow-Methods 和 Access-Control-Allow-Headers
- 响应头信息
Access-Control-Allow-Methods: POST, GET, OPTIONS
表明服务器允许客户端使用 POST, GET 和 OPTIONS 方法发起请求Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
表明服务器允许请求中携带字段 X-PINGOTHER 与 Content-Type
5、Access-Control-Expose-Headers
- 响应头信息,列出了哪些首部可以作为响应的一部分暴露给外部。
- 如果想要让客户端可以访问到其他的首部信息,可以将它们在 Access-Control-Expose-Headers 里面列出来。
6、Access-Control-Max-Age
- 响应头信息
Access-Control-Max-Age: 86400
表明该响应的有效时间为 86400 秒,也就是 24 小时。- 在有效时间内,浏览器无须为同一请求再次发起预检请求。
- 注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
7、Access-Control-Allow-Credentials
- 响应头信息,指定了当浏览器的credentials设置为true时是否允许浏览器读取response的内容
- 如果跨域请求携带了cookie信息,服务端必须设置
Access-Control-Allow-Credentials: true
,否则浏览器将不会把响应内容返回给请求的发送者 - 附带身份凭证的ajax请求
$.ajax({
url:'http://127.0.0.1:8080/test/cookie',
type:'GET',
xhrFields:{
withCredentials: true // 跨域请求想要带上cookie信息必须加上
},
success:function(data){
//请求成功相关处理
}
});
- 注意,对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为
*
,这是因为请求的首部中携带了 Cookie 信息,Access-Control-Allow-Origin 的值必须设置为具体的域名。
4、后端使用Filter添加 CORS 响应头信息
- 配置过滤器
@Bean
public FilterRegistrationBean<CorsFilter> registerFilter() {
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>();
bean.addUrlPatterns("/*");
bean.setFilter(new CorsFilter());
return bean;
}
- 过滤器
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String origin = httpServletRequest.getHeader("Origin");
if (!StringUtils.isEmpty(origin)) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String method = httpServletRequest.getMethod();
if (misMatchOrigin(origin) || misMatchMethod(method)) {
rejectRequest(httpServletResponse);
return;
}
// 带cookie的时候,origin必须是全匹配,不能使用*
httpServletResponse.addHeader("Access-Control-Allow-Origin", origin);
httpServletResponse.addHeader("Access-Control-Allow-Methods", "POST,GET,OPTION");
// 支持自定义请求头
String requestHeader = httpServletRequest.getHeader("Access-Control-Request-Headers");
if (!StringUtils.isEmpty(requestHeader)) {
httpServletResponse.addHeader("Access-Control-Allow-Headers", requestHeader);
}
httpServletResponse.addHeader("Access-Control-Max-Age", "3600");
httpServletResponse.addHeader("Access-Control-Allow-Credentials", "true");
if (HttpMethod.OPTIONS.matches(method)) {
return;
}
}
chain.doFilter(request, response);
}
private void rejectRequest(HttpServletResponse httpServletResponse) throws IOException {
httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value());
httpServletResponse.getOutputStream().write("Invalid CORS request".getBytes(StandardCharsets.UTF_8));
}
}