网上很多文章都是告诉你直接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 直接返回204h9 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 直接返回20415 if ($request_method = 'OPTIONS') {16 return 204;17 }18 }19 ...20 }
通过nginx配置,可以轻松解决跨域,OPTIONS预请求也交给了nginx完成,后端服务器则不用关心这个问题
后端解决
通过我们的后端服务器进行设置设置响应头与处理OPTIONS请求
_那么问题来了,后端要单独为预检请求做处理,那我们开发难道还需要单独写接口处理吗?。也没必要担心,因为springMvc已经为我们做了处理
_具体实现如下
private class PreFlightHandler implements HttpRequestHandler, CorsConfigurationSource {@Nullableprivate final CorsConfiguration config;public PreFlightHandler(@Nullable CorsConfiguration config) {this.config = config;}@Overridepublic void handleRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {corsProcessor.processRequest(this.config, request, response);}@Override@Nullablepublic 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会根据跨域配置处理跨域请求。
配置跨域
@Overridepublic 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 {// 访问前进行拦截@Overridepublic 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;}
}
配置拦截器
@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录registry.addInterceptor(reqIdHandler()).addPathPatterns("/**"); //验证corpId是否一致registry.addInterceptor(new CrosInterceptor()).addPathPatterns("/**");}
拦截器跳过OPTIONS请求
但是需要注意的是要防止服务器的拦截器拦截的OPTIONS请求,导致预检失败,报跨域错误
例如这样就会导致跨域报错。
@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader(Constants.TOKEN);if (StrUtil.isEmpty(token)) {throw new TokenException("Token为空");}}
应该为拦截器加判断
@Overridepublic 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为空");}}