一、什么是跨域
跨域,指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript施加的安全限制。
所谓同源是指,域名,协议,端口均相同,不明白没关系,举个栗子:
http://www.123.com/index.html 调用 http://www.123.com/server.PHP (非跨域)
http://www.123.com/index.html 调用 http://www.456.com/server.php
(主域名不同:123/456,跨域)http://abc.123.com/index.html 调用 http://def.123.com/server.php
(子域名不同:abc/def,跨域)http://www.123.com:8080/index.html 调用 http://www.123.com:8081/server.php (端口不同:8080/8081,跨域)
http://www.123.com/index.html 调用 https://www.123.com/server.php(协议不同:http/https,跨域)
请注意:localhost和127.0.0.1虽然都指向本机,但也属于跨域。
浏览器执行javascript脚本时,会检查这个脚本属于哪个页面,如果不是同源页面,就不会被执行。
- 同源策略限制内容有:
- Cookie、LocalStorage、IndexedDB 等存储性内容
- DOM 节点
- AJAX 请求不能发送
二、前端的解决办法
- JSONP:
使用方式就不赘述了,但是要注意JSONP只支持GET请求,不支持POST请求。
JSONP原理
ajax请求受同源策略影响,不允许进行跨域请求
,而script标签src属性中的链接却可以访问跨域的js脚本,利用这个特性,服务端不再返回JSON格式的数据,而是返回一段调用某个函数的js代码,在src中进行了调用,这样实现了跨域。
jsonp其实是一种特定的格式,一般是 fun(json格式参数),
服务端:
header('Content-type: application/json');
//获取回调函数名
$jsoncallback = htmlspecialchars($_REQUEST ['jsoncallback']);
//json数据
$json_data = '["customername1","customername2"]';
//输出jsonp格式的数据
echo $jsoncallback . "(" . $json_data . ")";
客户端:
function callbackFunction(result, methodName)
{
console.log(result[0]);
};
其实就是返回一个js文件,里面执行了callbackFunction方法;因为引用js文件不存在跨域的问题,这样问题就解决啦
~~~
- 代理(Nginx或者Vue-Cli)
例如www.123.com/index.html需要调用www.456.com/server.php,可以写一个接口www.123.com/server.php,由这个接口在后端去调用www.456.com/server.php并拿到返回值,然后再返回给index.html
,这就是一个代理的模式。相当于绕过了浏览器端,自然就不存在跨域问题。比如vue-cli中开启代理服务器的方式。
- vue.config.js的配置
devServer: {
proxy: {
'/atguigu': {
target: 'http://localhost:5000',
//路径重写,不然强求会带上前端vue自己配置的/atguigu请求后端,会造成后端解析不了报错
pathRewrite:{'^/atguigu':''},
// ws: true, //用于支持websocket
// changeOrigin: true //用于控制请求头中的host值
},
'/demo': {
target: 'http://localhost:5001',
pathRewrite:{'^/demo':''},
// ws: true, //用于支持websocket
// changeOrigin: true //用于控制请求头中的host值
}
}
- nginx反向代理
实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求
。使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
实现思路:通过nginx配置一个代理服务器做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。将nginx目录下的nginx.conf修改如下:
// proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Methods GET, POST, OPTIONS;
add_header Access-Control-Allow-Headers *;
}
}
这样我们的前端代理只要访问 http:www.domain1.com:81/*就可以了。
- 修改header(XHR2方式)
- header(‘Access-Control-Allow-Origin:*’);//允许所有来源访问
- header(‘Access-Control-Allow-Method:POST,GET’);//允许访问的方式
三、后端的解决方式
- 关于cors
CORS(Cross-origin resource sharing),跨域资源共享。CORS 其实是浏览器制定的一个规范,浏览器会自动进行 CORS 通信,它的实现则主要在服务端,它通过一些 HTTP Header 来限制可以访问的域,例如页面 A 需要访问 B 服务器上的数据,如果 B 服务器 上声明了允许 A 的域名访问,那么从 A 到 B 的跨域请求就可以完成。
对于那些会对服务器数据产生副作用的 HTTP 请求,浏览器会使用 OPTIONS 方法发起一个预检请求(preflight request),从而可以获知服务器端是否允许该跨域请求,服务器端确认允许后,才会发起实际的请求
。在预检请求的返回中,服务器端也可以告知客 户端是否需要身份认证信息。我们只需要设置响应头,即可进行跨域请求。
虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求
。
- 简单请求:
只要同时满足以下两大条件,就属于简单请求:
- 使用GET、HEAD、POST方法之一;
- Content-Type的值仅限于:text/plain、multipart/form-data、application/x-www-form-urlencoded,请求中的任意
XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用
XMLHttpRequest.upload 属性访问;
- 复杂请求:
不符合以上条件的请求就肯定是复杂请求了。 复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。
我们用PUT向后台请求时,属于复杂请求,后台需被请求的Servlet中添加Header设置,Access-Control-Allow-Origin这个Header在W3C标准里用来检查该跨域请求是否可以被通过,如果值为*则表明当前页面可以跨域访问。
默认的情况下是不允许的。
- 配置过滤器的方式:
@WebFilter(filterName = "corsFilter", urlPatterns = "/*",
initParams = {@WebInitParam(name = "allowOrigin", value = "*"),
@WebInitParam(name = "allowMethods", value = "GET,POST,PUT,DELETE,OPTIONS"),
@WebInitParam(name = "allowCredentials", value = "true"),
@WebInitParam(name = "allowHeaders", value = "Content-Type,X-Token")})
public class CorsFilter implements Filter {
private String allowOrigin;
private String allowMethods;
private String allowCredentials;
private String allowHeaders;
private String exposeHeaders;
@Override
public void init(FilterConfig filterConfig){
allowOrigin = filterConfig.getInitParameter("allowOrigin");
allowMethods = filterConfig.getInitParameter("allowMethods");
allowCredentials = filterConfig.getInitParameter("allowCredentials");
allowHeaders = filterConfig.getInitParameter("allowHeaders");
exposeHeaders = filterConfig.getInitParameter("exposeHeaders");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (!StringUtils.isEmpty(allowOrigin)) {
if(allowOrigin.equals("*")){
// 设置哪个源可以访问
response.setHeader("Access-Control-Allow-Origin", allowOrigin);
}else{
List<String> allowOriginList = Arrays.asList(allowOrigin.split(","));
if (allowOriginList.size() > 0) {
String currentOrigin = request.getHeader("Origin");
if (allowOriginList.contains(currentOrigin)) {
response.setHeader("Access-Control-Allow-Origin", currentOrigin);
}
}
}
}
if (!StringUtils.isEmpty(allowMethods)) {
//设置哪个方法可以访问
response.setHeader("Access-Control-Allow-Methods", allowMethods);
}
if (!StringUtils.isEmpty(allowCredentials)) {
// 允许携带cookie
response.setHeader("Access-Control-Allow-Credentials", allowCredentials);
}
if (!StringUtils.isEmpty(allowHeaders)) {
// 允许携带哪个头
response.setHeader("Access-Control-Allow-Headers", allowHeaders);
}
if (!StringUtils.isEmpty(exposeHeaders)) {
// 允许携带哪个头
response.setHeader("Access-Control-Expose-Headers", exposeHeaders);
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
- 基于SpringBoot: 以下代码配置也可以完美解决前后端跨域请求问题
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
final CorsConfiguration corsConfiguration = new CorsConfiguration();
/*是否允许请求带有验证信息,即支持cookies跨域*/
corsConfiguration.setAllowCredentials(true);
/*允许访问的客户端域名*/
corsConfiguration.addAllowedOrigin("*");
// corsConfiguration.setAllowedOrigins(List.of("*"));
/*允许服务端访问的客户端请求头*/
corsConfiguration.addAllowedHeader("*");
/*允许访问的方法名,GET POST等*/
corsConfiguration.addAllowedMethod("*");
// 缓存时间。在这个时间段内,相同的跨域请求将不再检查
corsConfiguration.setMaxAge(300L);
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}
- SpringBoot@CrossOrigin注解
这个方法仅对Java有用。springboot中,在Controller类上添加一个 @CrossOrigin(origins ="*") 注解就可以实现对当前controller 的跨域访问了
,当然这个标签也可以加到方法上,或者直接加到入口类上对所有接口进行跨域处理,注意这个注解只在JDK1.8版本以上才起作用。
- 使用SpringCloud网关
服务网关(zuul)又称路由中心,用来统一访问所有api接口,维护服务。Spring Cloud Zuul通过与Spring Cloud Eureka的整合
,实现了对服务实例的自动化维护,所以在使用服务路由配置的时候,我们不需要向传统路由配置方式那样去指定具体的服务实例地址,只需要通过Ant模式配置文件参数即可。当然SpringCloud网关和微服务调用中心不只有这一种组合。
四、总结
- CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案
- JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
- 不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。
日常工作中,用得比较多的跨域方案是cors和nginx反向代理