网上很多文章都是告诉你直接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,但是如果设置,预检请求和正式请求都需要设置)。不过不建议跨域使用(项目中用到过,不过不稳定,有些浏览器带不过去),除非必要,因为有很多方案可以代替。
什么是预检请求?:当发生跨域条件时候,览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。如下图
这个请求就是**OPTIONS请求,因此服务器想要支持跨域请求,那么就需要对OPTIONS**请求做支持。
那么使用nginx就可以完全支持跨域,
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
如果有这个需求,需要自行编写拦截器进行处理。
/ 跨域拦截器
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-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("/**");
}
拦截器跳过OPTIONS请求
但是需要注意的是要防止服务器的拦截器拦截的OPTIONS请求,导致预检失败,报跨域错误
例如这样就会导致跨域报错。
@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为空");
}
}
应该为拦截器加判断
@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为空");
}
}