彻底解决跨域问题vue+Springboot
1. 背景
- 前端地址:http://localhost:9500
- 后端地址:http://localhost:11000
- 问题一:访问简单请求出错
- 问题二:访问非简单请求出错
浏览器将CORS请求分为两类:简单请求和非简单请求。
只要满足以下两个条件,就是简单请求,否则就是非简单请求。
- 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
- HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
2. 解决过程
2.1. 解决方案一
2.11. 解析错误信息
Access to XMLHttpRequest at 'http://localhost:11000/api/plugin/list' from origin 'http://localhost:9500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
翻译:来自’http://localhost:9500’的 'http://localhost:11000/api/plugin/list’请求已经被CORS policy拦截了。在被请求的资源上没有“Access-Control-Allow-Origin”头信息。
意思是:请求的资源已经返回了,但是返回的资源里没有“Access-Control-Allow-Origin”头信息,所以被浏览器的CORS策略拦截了。查看服务端的日志信息可以发现,请求确实成功返回了。
-
首先,这个错误是由于浏览器的同源策略导致的跨域问题。同源策略要求协议(http)、端口(9500)、域名(localhost)都相同。其中一个不同,都被当做是不同的域。9500和11000端口不同,所以被CORS策略blocked。
-
CORS:Cross-origin resource sharing,跨域资源共享。允许浏览器访问跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
2.12 问题一出现的过程分析
- 问题一属于简单的GET请求,浏览器直接发出CORS请求。具体来说就是头信息中,增加一个Origin字段。
上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
- Access-Control-Allow-Origin: http://api.bob.com
- Access-Control-Allow-Credentials: true
- Access-Control-Expose-Headers: FooBar
- Content-Type: text/html; charset=utf-8
上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。
(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
(3)Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader(‘FooBar’)可以返回FooBar字段的值。
2.13 解决方案之服务端设置允许跨域
- 方案1. controller对应的方法里添加“Access-Control-Allow-Origin”
@GetMapping("/list")
public List<PluginResponse> getUncheckPlugins(HttpServletResponse response) {
response.setHeader("Access-Control-Allow-Origin", "*");
return pluginService.getcheckedPlugins(); }
但是,这种方法需要在服务端每一个接口里面设置,传参还需多加一个HttpServletResponse response,超级不优雅,不推荐!!!
- 方案2. 使用spring框架提供的注解@CrossOrigin
- 方案3. 使用WebMvcConfigurer接口、拦截器、过滤器
- Springboot 通过实现WebMvcConfigurer接口来定制Spring MVC配置,例如拦截器。WebMvcConfigurer接口提供了
default void addCorsMappings(CorsRegistry registry) {}
方法,专门用来处理跨域请求。
定义一个类实现WebMvcConfigurer接口,使用注解@Configuration注入Springboot loC容器
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedMethods("GET", "POST", "HEAD", "PUT");
}
}
- 使用拦截器,稍微比上面麻烦了点,实现此方案纯粹为了帮助理解跨域问题。不推荐!!!
使用拦截器第一步:定义拦截器
public class MyHandleInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
boolean res = HandlerInterceptor.super.preHandle(request, response, handler);
response.setHeader("Access-Control-Allow-Origin", "*");
return res;
}
// @Override
// public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView){
// response.setHeader("Access-Control-Allow-Origin", "*");
// Logger.getLogger("MyHandleInterceptor").info("进入postHandle");
// }
}
注意,一开始使用postHandle方法实现,但是设置response的header信息不起效,查信息和看源码后,postHandler在response信息生成之后,此时ModelAndView已经返回。要想response生效,需要在preHandle中处理。
第二步:注册拦截器 + 第三步:指定拦截规则
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyHandleInterceptor()).addPathPatterns("/api/**");
}
}
- 过滤器使用代码不做实现。
2.2. 解决方案二
2.21. 了解nginx
nginx中文文档
看了概述还是不知道nginx干嘛的,没关系,我们只需了解nginx可以转发请求即可。
2.22. 解决方案之nginx
使用nginx第一步:下载nginx
使用nginx第二步:修改conf目录下的配置文件nginx.conf
原先的配置基本不用修改,关键部分代码如下:
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 8088;
server_name localhost;
location /api {
proxy_pass http://127.0.0.1:11000/api;
#告诉浏览器允许跨域访问的方法
add_header Access-Control-Allow-Methods *;
# 告诉浏览器缓存OPTIONS预检请求1小时
add_header Access-Control-Max-Age 3600;
#允许带有cookie访问
add_header Access-Control-Allow-Credentials true;
#注意 * 不能满足带有cookie的访问,Origin 必须是全匹配,这里通过变量获取
add_header Access-Control-Allow-Origin $http_origin;
#设置支持所有的自定义请求头
add_header Access-Control-Allow-Headers $http_access_control_request_headers;
#如果预检请求,则返回成功,不需要转发到后端
if ($request_method = OPTIONS){
return 200;
}
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
Nginx监听8088端口,前端需要将目的地址的端口改为8088。add_header指令用于添加返回头字段。可以看到配置中加了跨域相关的配置信息。
3. 总结
- nginx在项目中使用非常普遍,建议使用nginx直接配置。
- 网上很多方案是前端需要配置跨域信息,我个人理解是前端无需设置。配置了也没有任何作用。
- 以上过程都已实证,大家有问题可以交流。