请求跨域处理 Nginx处理和后端处理

376 阅读11分钟

什么事跨域?为什么会出现跨域?

同源策略

由于浏览器避免各种攻击和注入,如

  • 跨站脚本攻击(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 预检请求。这样的请求为 简单请求,。若请求满足所有下述条件,则该请求可视为 简单请求

  1. HTTP 方法限制:只能使用 GET、HEAD、POST 这三种 HTTP 方法之一。如果请求使用了其他 HTTP 方法,就不再被视为简单请求。
  2. 自定义标头限制:请求的 HTTP 标头只能是以下几种常见的标头:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(仅限于 application/x-www-form-urlencodedmultipart/form-datatext/plain)。HTML 头部 header field 字段:DPR**、Download、Save-Data、Viewport-Width、WIdth。如果请求使用了其他标头,同样不再被视为简单请求。
  3. 请求中没有使用 ReadableStream** 对象。
  4. 不使用自定义请求标头:请求不能包含用户自定义的标头。
  5. 请求中的任意 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-开头。

image.png

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都可以自行修改

image.png

非简单请求是对那种对服务器有特殊要求的请求,比如请求方式是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

image.png

如果没有

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

image.png

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 的时候这么写

image.png

blog.csdn.net/MyZy_/artic…