为什么我会收到 jQuery 的 CORS 错误,而 XMLHttpRequest 却没有?

Why am I getting a CORS error with jQuery but not with XMLHttpRequest?

我们在从 UI 代码库进行跨域 API 调用时遇到了奇怪的问题,其中 Ajax 请求(vanilla JS)的一种方式有效,但另一种无效(jQuery)。任何指针都会有所帮助。

跨域成功调用

let url = 'https://somedomain.com/test-api/links';
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = 'application/json';
xhr.onload = function () {
  // Successful request
  if (xhr.status == 200) {
    console.log('success')
  }
};
xhr.onerror = function () {
  // Crossdomain request denied
  if (xhr.status === 0) {
    console.log(xhr.response)
  }
};

xhr.crossDomain = true;
xhr.withCredentials = true;
xhr.send();

然而,使用以下代码库 (jQuery) 我们收到错误消息 -:

Access to XMLHttpRequest at 'https://sourcedomain.com/test-api/links' from origin 'https://callerDomain.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute

.ajax({
      url: "https://somedomain.com/test-api/links",
      method: 'GET',
      contentType: 'application/json',
      dataType: "json",
      crossDomain : true,
      xhrFields: {
        withCredentials: true
      }
    }).then((resp) => {
      console.log(resp)
    }).catch((err) => {
      console.log(err)
    });

sourcedomain.com

上的 Apache 配置
Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE, PUT"
Header set Access-Control-Allow-Credentials true
Header always set Access-Control-Allow-Origin "https://callerDomain.com"
Header always set Access-Control-Allow-Headers "*"

更新: 由于以下组合无效,我将 apacahe 配置更新为

 Header always set Access-Control-Allow-Headers "*"
 Header set Access-Control-Allow-Credentials true

Apache 在 sourcedomain.com

上更新了配置
Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE, PUT"
Header set Access-Control-Allow-Credentials true
Header always set Access-Control-Allow-Origin "https://callerDomain.com"
//for testing only
Header set   Access-Control-Allow-Headers       "Accept, Accept-CH, Accept-Charset, Accept-Datetime, Accept-Encoding, Accept-Ext, Accept-Features, Accept-Language, Accept-Params, Accept-Ranges, Access-Control-Allow-Credentials, Access-Control-Allow-Headers, Access-Control-Allow-Methods, Access-Control-Allow-Origin, Access-Control-Expose-Headers, Access-Control-Max-Age, Access-Control-Request-Headers, Access-Control-Request-Method, Age, Allow, Alternates, Authentication-Info, Authorization, C-Ext, C-Man, C-Opt, C-PEP, C-PEP-Info, CONNECT, Cache-Control, Compliance, Connection, Content-Base, Content-Disposition, Content-Encoding, Content-ID, Content-Language, Content-Length, Content-Location, Content-MD5, Content-Range, Content-Script-Type, Content-Security-Policy, Content-Style-Type, Content-Transfer-Encoding, Content-Type, Content-Version, Cookie, Cost, DAV, DELETE, DNT, DPR, Date, Default-Style, Delta-Base, Depth, Derived-From, Destination, Differential-ID, Digest, ETag, Expect, Expires, Ext, From, GET, GetProfile, HEAD, HTTP-date, Host, IM, If, If-Match, If-Modified-Since, If-None-Match, If-Range, If-Unmodified-Since, Keep-Alive, Label, Last-Event-ID, Last-Modified, Link, Location, Lock-Token, MIME-Version, Man, Max-Forwards, Media-Range, Message-ID, Meter, Negotiate, Non-Compliance, OPTION, OPTIONS, OWS, Opt, Optional, Ordering-Type, Origin, Overwrite, P3P, PEP, PICS-Label, POST, PUT, Pep-Info, Permanent, Position, Pragma, ProfileObject, Protocol, Protocol-Query, Protocol-Request, Proxy-Authenticate, Proxy-Authentication-Info, Proxy-Authorization, Proxy-Features, Proxy-Instruction, Public, RWS, Range, Referer, Refresh, Resolution-Hint, Resolver-Location, Retry-After, Safe, Sec-Websocket-Extensions, Sec-Websocket-Key, Sec-Websocket-Origin, Sec-Websocket-Protocol, Sec-Websocket-Version, Security-Scheme, Server, Set-Cookie, Set-Cookie2, SetProfile, SoapAction, Status, Status-URI, Strict-Transport-Security, SubOK, Subst, Surrogate-Capability, Surrogate-Control, TCN, TE, TRACE, Timeout, Title, Trailer, Transfer-Encoding, UA-Color, UA-Media, UA-Pixels, UA-Resolution, UA-Windowpixels, URI, Upgrade, User-Agent, Variant-Vary, Vary, Version, Via, Viewport-Width, WWW-Authenticate, Want-Digest, Warning, Width, X-Content-Duration, X-Content-Security-Policy, X-Content-Type-Options, X-CustomHeader, X-DNSPrefetch-Control, X-Forwarded-For, X-Forwarded-Port, X-Forwarded-Proto, X-Frame-Options, X-Modified, X-OTHER, X-PING, X-PINGOTHER, X-Powered-By, X-Requested-With"

响应预检请求

问题

两个请求不等价

对于通过 XHR 发送的,您只需设置 content type of the response:

xhr.open("GET", url, true);
// ...
xhr.responseType = 'application/json';

但是,对于通过 jQuery 发送的请求,您将 content type of the request itself 设置为 application/json:

.ajax({
  // ...
  method: 'GET',
  contentType: 'application/json',
  // ...
});

将通配符与凭据请求结合使用

请求内容类型的 application/json 值使其成为 non-simple;因此,您的浏览器会触发预检请求。但是,您使用

响应此预检请求
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true

这是一个 incompatible combination。因此,CORS 预检失败,如浏览器错误消息中所述。

总是以 403 状态

响应预检请求

您最近的编辑显示预检请求的响应状态为 403。但是,要使 CORS 预检成功,状态必须在 2xx 范围内;请参阅 this other recent answer of mine

解决方案

  1. 要么不指定请求的 content-type 以保留它 simple,要么(最好)明确允许 Access-Control-Allow-Headers 中的 Content-Type header header 而不是使用通配符。
  2. 确保以 2xx 状态响应成功的预检请求。请注意,由于预检请求从不携带凭据,因此您应该在任何授权中间件之前处理此类请求。