什么事跨域?为什么会出现跨域?
同源策略
由于浏览器避免各种攻击和注入,如
- 跨站脚本攻击(XSS)
- SQL 注入攻击
- OS 命令注入攻击
- HTTP 首部注入攻击
- 跨站点请求伪造(CSRF)
- 等等......
因此有同源策略,主要限制:DOM 访问限制、Web 数据限制、网络通信限制等
如果两个 URL 的协议、主机和端口都相同,我们就称这两个 URL 同源。
- 协议:协议是定义了数据如何在计算机内和之间进行交换的规则的系统,例如 HTTP、HTTPS。
- 主机:是已连接到一个计算机网络的一台电子计算机或其他设备。网络主机可以向网络上的用户或其他节点提供信息资源、服务和应用。使用 TCP/IP 协议族参与网络的计算机也可称为 IP 主机。
- 端口:主机是计算机到计算机之间的通信,那么端口就是进程到进程之间的通信。
为了避免浏览器的这种同源策略限制,因此出现了跨域请求即 跨源资源共享。
跨源资源共享
跨源资源共享(Cross-Origin Resource Sharing,CORS)是一种机制,允许在受控的条件下,不同源的网页能够请求和共享资源。由于浏览器的同源策略限制了跨域请求,CORS 提供了一种方式来解决在 Web 应用中进行跨域数据交换的问题。
CORS 的基本思想是,服务器在响应中提供一个标头(HTTP 头),指示哪些源被允许访问资源。浏览器在发起跨域请求时会先发送一个预检请求(OPTIONS 请求)到服务器,服务器通过设置适当的 CORS 标头来指定是否允许跨域请求,并指定允许的请求源、方法、标头等信息。
简单请求
不会触发 CORS 预检请求。这样的请求为 简单请求,。若请求满足所有下述条件,则该请求可视为 简单请求:
- HTTP 方法限制:只能使用 GET、HEAD、POST 这三种 HTTP 方法之一。如果请求使用了其他 HTTP 方法,就不再被视为简单请求。
- 自定义标头限制:请求的 HTTP 标头只能是以下几种常见的标头:
Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(仅限于application/x-www-form-urlencoded、multipart/form-data、text/plain)。HTML 头部 header field 字段:DPR**、Download、Save-Data、Viewport-Width、WIdth。如果请求使用了其他标头,同样不再被视为简单请求。 - 请求中没有使用 ReadableStream** 对象。
- 不使用自定义请求标头:请求不能包含用户自定义的标头。
- 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问
预检请求
非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 预检请求。
需预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求 的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
Nginx 跨域处理 add_header Access-Control-*
网上很多文章都是告诉你直接Nginx添加这几个响应头信息就能解决跨域,当然大部分情况是能解决,但是我相信还是有很多情况,明明配置上了,也同样会报跨域问题。
这大概率是因为,服务端没有正确处理预检请求也就是OPTIONS请求
CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight);浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。 跨域主要涉及4个响应头:
- Access-Control-Allow-Origin 用于设置允许跨域请求源地址 (预检请求和正式请求在跨域时候都会验证)
- Access-Control-Allow-Headers 跨域允许携带的特殊头信息字段 (只在预检请求验证)
- Access-Control-Allow-Methods 跨域允许的请求方法或者说HTTP动词 (只在预检请求验证)
- Access-Control-Allow-Credentials 是否允许跨域使用cookies,如果要跨域使用cookies,可以添加上此请求响应头,值设为true(设置或者不设置,都不会影响请求发送,只会影响在跨域时候是否要携带cookies,但是如果设置,预检请求和正式请求都需要设置)。不过不建议跨域使用(项目中用到过,不过不稳定,有些浏览器带不过去),除非必要,因为有很多方案可以代替。
为什么有预检请求?
浏览器将CORS请求分为两类:简单请求(simple request)和非简单请求(not-simple-request),简单请求浏览器不会预检,而非简单请求会预检。
同时满足下列三大条件,就属于简单请求,否则属于非简单请求
1.请求方式只能是:GET、POST、HEAD
2.HTTP请求头限制这几种字段:Accept、Accept-Language、Content-Language、Content-Type、Last-Event-ID
3.Content-type只能取:application/x-www-form-urlencoded、multipart/form-data、text/plain
对于简单请求,浏览器直接请求,会在请求头信息中,增加一个origin字段,来说明本次请求来自哪个源(协议+域名+端口)。服务器根据这个值,来决定是否同意该请求,服务器返回的响应会多几个头信息字段,如图所示:下面的头信息中,与CORS请求相关,都是以Access-Control-开头。
1.Access-Control-Allow-Origin:该字段是必须的,* 表示接受任意域名的请求,还可以指定域名
2.Access-Control-Allow-Credentials:该字段可选,是个布尔值,表示是否可以携带cookie,(注意:如果Access-Control-Allow-Origin字段设置*,此字段设为true无效)
3.Access-Control-Allow-Headers:该字段可选,里面可以获取Cache-Control、Content-Type、Expires等,如果想要拿到其他字段,就可以在这个字段中指定。比如图中指定的token
而Request Headers则是前端这边请求的头部,origin为浏览器添加,accept、content-type、还有自定义的token都可以自行修改
非简单请求是对那种对服务器有特殊要求的请求,比如请求方式是PUT或者DELETE,或者Content-Type字段类型是application/json,或者带有自定义头部的。都会在正式通信之前,增加一次HTTP请求,称之为预检。 浏览器会先询问服务器,当前网页所在域名是否在服务器的许可名单之中,服务器允许之后,浏览器会发出正式的XMLHttpRequest请求,否则会报错。后端可以实现拦截器,排除Options。
比如,在nginx服务器的server里或location里写
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Methods 'POST, GET, OPTIONS, DELETE';
add_header Access-Control-Max-Age "3600";
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Headers 'token,Content-Type, x-requested-with, X-Custom-Header, Request-Ajax';
add_header Content-Type 'application/json; charset=utf-8';
if ($request_method = 'OPTIONS') {
return 200;
}
如果没有add_header Access-Control-Allow-Origin '*';会报错No 'Access-Control-Allow-Origin' header is present on the requested resource
如果没有
add_header Access-Control-Allow-Methods 'POST, GET, OPTIONS, DELETE';
和
add_header Access-Control-Allow-Headers 'token,Content-Type, x-requested-with, X-Custom-Header, Request-Ajax';
会报错 Request header field token is not allowed by Access-Control-Allow-Headers in preflight response
if ($request_method = 'OPTIONS') {
return 200;
}
优势会遇到请求接口返回状态码非200跨域的问题,那是因为add_header只有在 200, 201 (1.3.10), 204, 206, 301, 302, 303, 304, 307 (1.1.16, 1.0.13), or 308 (1.13.0). 才会生效,其他的状态码要生效的话,就得添加 always这个参数
改为
add_header Access-Control-Allow-Origin '*' always;
跨域Nginx的后端与前端处理方法 blog.csdn.net/qq_37436172…
Nginx跨域处理:使用nginx进行设置设置响应头与处理OPTIONS请求
方案1 *:通配符,全部允许,存在安全隐患(不推荐)。
一旦启用本方法,表示任何域名皆可直接跨域请求:
1 server {
2 ...
3 location / {
4 # 允许 所有头部 所有域 所有方法
5 add_header 'Access-Control-Allow-Origin' '*';
6 add_header 'Access-Control-Allow-Headers' '*';
7 add_header 'Access-Control-Allow-Methods' '*';
8 # OPTIONS 直接返回204h
9 if ($request_method = 'OPTIONS') {
10 return 204;
11 }
12 }
13 ...
14 }
方案2:多域名配置(推荐)
配置多个域名在map中 只有配置过的允许跨域:
1 map $http_origin $corsHost {
2 default 0;
3 "~https://zzzmh.cn" https://zzzmh.cn;
4 "~https://chrome.zzzmh.cn" https://chrome.zzzmh.cn;
5 "~https://bz.zzzmh.cn" https://bz.zzzmh.cn;
6 }
7 server {
8 ...
9 location / {
10 # 允许 所有头部 所有$corsHost域 所有方法
11 add_header 'Access-Control-Allow-Origin' $corsHost;
12 add_header 'Access-Control-Allow-Headers' '*';
13 add_header 'Access-Control-Allow-Methods' '*';
14 # OPTIONS 直接返回204
15 if ($request_method = 'OPTIONS') {
16 return 204;
17 }
18 }
19 ...
20 }
通过nginx配置,可以轻松解决跨域,OPTIONS预请求也交给了nginx完成,后端服务器则不用关心这个问题
后端解决
通过我们的后端服务器进行设置设置响应头与处理OPTIONS请求
_那么问题来了,后端要单独为预检请求做处理,那我们开发难道还需要单独写接口处理吗?。也没必要担心,因为springMvc已经为我们做了处理
_具体实现如下
private class PreFlightHandler implements HttpRequestHandler, CorsConfigurationSource {
@Nullable
private final CorsConfiguration config;
public PreFlightHandler(@Nullable CorsConfiguration config) {
this.config = config;
}
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
corsProcessor.processRequest(this.config, request, response);
}
@Override
@Nullable
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
return this.config;
}
}
public class DefaultCorsProcessor implements CorsProcessor {
private static final Log logger = LogFactory.getLog(DefaultCorsProcessor.class);
@Override
@SuppressWarnings("resource")
public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request,
HttpServletResponse response) throws IOException {
Collection<String> varyHeaders = response.getHeaders(HttpHeaders.VARY);
if (!varyHeaders.contains(HttpHeaders.ORIGIN)) {
response.addHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN);
}
if (!varyHeaders.contains(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD)) {
response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD);
}
if (!varyHeaders.contains(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS)) {
response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
}
if (!CorsUtils.isCorsRequest(request)) {
return true;
}
if (response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
return true;
}
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
rejectRequest(new ServletServerHttpResponse(response));
return false;
}
else {
return true;
}
}
return handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);
}
protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
CorsConfiguration config, boolean preFlightRequest) throws IOException {
String requestOrigin = request.getHeaders().getOrigin();
String allowOrigin = checkOrigin(config, requestOrigin);
HttpHeaders responseHeaders = response.getHeaders();
if (allowOrigin == null) {
logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
rejectRequest(response);
return false;
}
HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
if (allowMethods == null) {
logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
rejectRequest(response);
return false;
}
List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
List<String> allowHeaders = checkHeaders(config, requestHeaders);
if (preFlightRequest && allowHeaders == null) {
logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
rejectRequest(response);
return false;
}
responseHeaders.setAccessControlAllowOrigin(allowOrigin);
if (preFlightRequest) {
responseHeaders.setAccessControlAllowMethods(allowMethods);
}
if (preFlightRequest && !allowHeaders.isEmpty()) {
responseHeaders.setAccessControlAllowHeaders(allowHeaders);
}
if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
}
if (Boolean.TRUE.equals(config.getAllowCredentials())) {
responseHeaders.setAccessControlAllowCredentials(true);
}
if (preFlightRequest && config.getMaxAge() != null) {
responseHeaders.setAccessControlMaxAge(config.getMaxAge());
}
response.flush();
return true;
}
springMvc会根据跨域配置处理跨域请求。
配置跨域
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH");
}
可以用通过配置类进行配置上面的CorsConfiguration
不过新版的springMvc已经禁止同时设置
allowCredentials为true
allowCredentials为false
如果有这个需求,需要自行编写拦截器进行处理。
本质:设置后端服务器允许任何域名和方法的处理,避免Option等方法的拦截器校验处理
1.自定义跨域拦截器
/ 跨域拦截器
public class CrosInterceptor implements HandlerInterceptor {
// 访问前进行拦截
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) {
// 设置响应头
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*"); // 允许所有域名跨域访问
httpServletResponse.setHeader("Access-Control-Allow-Headers", "*");// 允许的请求方法
// httpServletResponse.setHeader("Access-Control-Allow-Headers", "Content-Type"); // 允许的请求头
// httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); // 允许的请求方法
httpServletResponse.setHeader("Access-Control-Allow-Methods", "*");
httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
return true;
}
}
配置拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
registry.addInterceptor(reqIdHandler())
.addPathPatterns("/**"); //验证corpId是否一致
registry.addInterceptor(new CrosInterceptor())
.addPathPatterns("/**");
}
或者
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private CorsInterceptor corsInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(corsInterceptor).addPathPatterns("/**"); // 对所有路径应用跨域设置
}
}
2.拦截器跳过OPTIONS请求
但是需要注意的是要防止服务器的拦截器拦截的OPTIONS请求,导致预检失败,报跨域错误
例如这样就会导致跨域报错。
因为在Spring Boot中,跨域请求可能会遇到预检请求(OPTIONS请求),这是浏览器行为,用于确定实际请求是否安全可接受。如果你的应用程序中有一个全局的拦截器,你可能不希望该拦截器对每个请求都进行处理,包括OPTIONS请求。
如不希望下面这个校验token的拦截器也校验Option 类型请求是否含有token
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader(Constants.TOKEN);
if (StrUtil.isEmpty(token)) {
throw new TokenException("Token为空");
}
}
为了让拦截器跳过OPTIONS请求,你可以在拦截器的实现中添加一个条件,判断当前请求是否为OPTIONS,如果是,则直接放行。
应该为校验token的拦截器加如下判断,如下
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if("OPTIONS".equals(request.getMethod())){
return true;
}
String token = request.getHeader(Constants.TOKEN);
if (StrUtil.isEmpty(token)) {
throw new TokenException("Token为空");
}
}
但是这样也还是比较麻烦,因为不可能为所有的拦截器加以上判断,所以在CrosInterceptor 拦截器上这么处理。只要是Option请求后续的拦截器校验都不处理
优化后的定义跨域拦截器 CrosInterceptor 的时候这么写