跨源资源共享(CORS,或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。
跨源 HTTP 请求的一个例子:运行在 https://domain-a.com
的 JavaScript 代码使用 XMLHttpRequest
来发起一个到 https://domain-b.com/data.json
的请求。
出于安全性,浏览器限制脚本内发起的跨源 HTTP 请求。例如,XMLHttpRequest
和 Fetch API
遵循同源策略。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。
CORS 机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest
或 Fetch
)使用 CORS,以降低跨源 HTTP 请求所带来的风险。
理论解释
功能概述
跨源资源共享标准新增了一组 HTTP 标头字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET
以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST
请求),浏览器必须首先使用 OPTIONS
方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(例如 Cookie 和 HTTP 认证相关数据)。
CORS 请求失败会产生错误,但是为了安全,在 JavaScript 代码层面无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。
若干访问控制场景
这里,我们使用三个场景来解释跨源资源共享机制的工作原理。这些例子都使用在任意所支持的浏览器上都可以发出跨域请求的 XMLHttpRequest
对象。
简单请求
某些请求不会触发 CORS 预检请求。在废弃的 CORS 规范中称这样的请求为简单请求,但是目前的 Fetch 规范(CORS 的现行定义规范)中不再使用这个词语。
其动机是,HTML 4.0 中的 <form>
元素(早于跨站 XMLHttpRequest
或 Fetch
)可以向任何来源提交简单请求,所以任何编写服务器的人一定已经在保护跨站请求伪造攻击(CSRF)。在这个假设下,服务器不必选择加入(通过响应预检请求)来接收任何看起来像表单提交的请求,因为 CSRF 的威胁并不比表单提交的威胁差。然而,服务器仍然必须提供 Access-Control-Allow-Origin
的选择,以便与脚本共享响应。
若请求满足所有下述条件,则该请求可视为简单请求:
- 使用下列方法之一:
- 除了被用户代理自动设置的标头字段(例如
Connection
、User-Agent
或其他在 Fetch 规范中定义为禁用标头名称的标头),允许人为设置的字段为 Fetch 规范定义的对 CORS 安全的标头字段集合。该集合为:Accept
Accept-Language
Content-Language
Content-Type
(需要注意额外的限制)Range
(只允许简单的范围标头值 如bytes=256-
或bytes=127-255
)
备注: Firefox 还没有将 Range
实现为安全的请求标头。参见 bug 1733981。
Content-Type
标头所指定的媒体类型的值仅限于下列三者之一:text/plain
multipart/form-data
application/x-www-form-urlencoded
- 如果请求是使用
XMLHttpRequest
对象发出的,在返回的XMLHttpRequest.upload
对象属性上没有注册任何事件监听器;也就是说,给定一个XMLHttpRequest
实例xhr
,没有调用xhr.upload.addEventListener()
,以监听该上传请求。 - 请求中没有使用
ReadableStream
对象。
备注: WebKit Nightly 和 Safari Technology Preview 为 Accept
、Accept-Language
和 Content-Language
标头字段的值添加了额外的限制。如果这些标头字段的值是“非标准”的,WebKit/Safari 就不会将这些请求视为“简单请求”。WebKit/Safari 并没有在文档中列出哪些值是“非标准”的,不过我们可以在这里找到相关讨论:
- Require preflight for non-standard CORS-safelisted request headers Accept, Accept-Language, and Content-Language
- Allow commas in Accept, Accept-Language, and Content-Language request headers for simple CORS
- Switch to a blacklist model for restricted Accept headers in simple CORS requests
其他浏览器并不支持这些额外的限制,因为它们不属于规范的一部分。
比如说,假如站点 https://foo.example
的网页应用想要访问 https://bar.other
的资源。foo.example
的网页中可能包含类似于下面的 JavaScript 代码:
const xhr = new XMLHttpRequest();
const url = 'https://bar.other/resources/public-data/';
xhr.open('GET', url);
xhr.onreadystatechange = someHandler;
xhr.send();
此操作实行了客户端和服务器之间的简单交换,使用 CORS 标头字段来处理权限:
以下是浏览器发送给服务器的请求报文:
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
请求标头字段 Origin
表明该请求来源于 http://foo.example
。
让我们来看看服务器如何响应:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
[…XML Data…]
本例中,服务端返回的 Access-Control-Allow-Origin
标头的 Access-Control-Allow-Origin: *
值表明,该资源可以被任意外源访问。
Access-Control-Allow-Origin: *
使用 Origin
和 Access-Control-Allow-Origin
就能完成最简单的访问控制。如果 https://bar.other
的资源持有者想限制他的资源只能通过 https://foo.example
来访问(也就是说,非 https://foo.example
域无法通过跨源访问访问到该资源),他可以这样做:
Access-Control-Allow-Origin: https://foo.example
备注: 当响应的是附带身份凭证的请求时,服务端必须明确 Access-Control-Allow-Origin
的值,而不能使用通配符“*
”。
预检请求
与简单请求不同,“需预检的请求”要求必须首先使用 OPTIONS
方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。”预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
如下是一个需要执行预检请求的 HTTP 请求:
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://bar.other/resources/post-here/');
xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');
上面的代码使用 POST
请求发送一个 XML 请求体,该请求包含了一个非标准的 HTTP X-PINGOTHER
请求标头。这样的请求标头并不是 HTTP/1.1 的一部分,但通常对于 web 应用很有用处。另外,该请求的 Content-Type
为 application/xml
,且使用了自定义的请求标头,所以该请求需要首先发起“预检请求”。
备注: 如下所述,实际的 POST
请求不会携带 Access-Control-Request-*
标头,它们仅用于 OPTIONS
请求。
下面是服务端和客户端完整的信息交互。首次交互是预检请求/响应:
OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
从上面的报文中,我们看到,第 1 - 10 行使用 OPTIONS
方法发送了预检请求,浏览器根据上面的 JavaScript 代码片断所使用的请求参数来决定是否需要发送,这样服务器就可以回应是否可以接受用实际的请求参数来发送请求。OPTIONS 是 HTTP/1.1 协议中定义的方法,用于从服务器获取更多信息,是安全的方法。该方法不会对服务器资源产生影响。注意 OPTIONS 预检请求中同时携带了下面两个标头字段:
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
标头字段 Access-Control-Request-Method
告知服务器,实际请求将使用 POST
方法。标头字段 Access-Control-Request-Headers
告知服务器,实际请求将携带两个自定义请求标头字段:X-PINGOTHER
与 Content-Type
。服务器据此决定,该实际请求是否被允许。
第 12 - 21 行为预检请求的响应,表明服务器将接受后续的实际请求方法(POST
)和请求头(X-PINGOTHER
)。重点看第 15 - 18 行:
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
服务器的响应携带了 Access-Control-Allow-Origin: https://foo.example
,从而限制请求的源域。同时,携带的 Access-Control-Allow-Methods
表明服务器允许客户端使用 POST
和 GET
方法发起请求(与 Allow
响应标头类似,但该标头具有严格的访问控制)。
标头字段 Access-Control-Allow-Headers
表明服务器允许请求中携带字段 X-PINGOTHER
与 Content-Type
。与 Access-Control-Allow-Methods
一样,Access-Control-Allow-Headers
的值为逗号分割的列表。
最后,标头字段 Access-Control-Max-Age
给定了该预检请求可供缓存的时间长短,单位为秒,默认值是 5 秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。以上例子中,该响应的有效时间为 86400 秒,也就是 24 小时。请注意,浏览器自身维护了一个最大有效时间,如果该标头字段的值超过了最大有效时间,将不会生效。
预检请求完成之后,发送实际请求:
POST /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache
<person><name>Arun</name></person>
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
[Some XML payload]
预检请求与重定向
并不是所有浏览器都支持预检请求的重定向。如果一个预检请求发生了重定向,一部分浏览器将报告错误:
The request was redirected to ‘https://example.com/foo‘, which is disallowed for cross-origin requests that require preflight. Request requires preflight, which is disallowed to follow cross-origin redirects.
CORS 最初要求浏览器具有该行为,不过在后续的修订中废弃了这一要求。但并非所有浏览器都实现了这一变更,而仍然表现出最初要求的行为。
在浏览器的实现跟上规范之前,有两种方式规避上述报错行为:
- 在服务端去掉对预检请求的重定向;
- 将实际请求变成一个简单请求。
如果上面两种方式难以做到,我们仍有其他办法:
- 发出一个简单请求(使用
Response.url
或XMLHttpRequest.responseURL
)以判断真正的预检请求会返回什么地址。 - 发出另一个请求(真正的请求),使用在上一步通过
Response.url
或XMLHttpRequest.responseURL
获得的 URL。
不过,如果请求是由于存在 Authorization
字段而引发了预检请求,则这一方法将无法使用。这种情况只能由服务端进行更改。
附带身份凭证的请求
备注: 当发出跨源请求时,第三方 cookie 策略仍将适用。无论如何改变本章节中描述的服务器和客户端的设置,该策略都会强制执行。
XMLHttpRequest
或 Fetch 与 CORS 的一个有趣的特性是,可以基于 HTTP cookies 和 HTTP 认证信息发送身份凭证。一般而言,对于跨源 XMLHttpRequest
或 Fetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest
对象的某个特殊标志位,或在构造 Request
对象时设置。
本例中,https://foo.example
的某脚本向 https://bar.other
发起一个 GET 请求,并设置 Cookies。在 foo.example
中可能包含这样的 JavaScript 代码:
const invocation = new XMLHttpRequest();
const url = "https://bar.other/resources/credentialed-content/";
function callOtherDomain() {
if (invocation) {
invocation.open("GET", url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}
第 7 行将 XMLHttpRequest
的 withCredentials
标志设置为 true
,从而向服务器发送 Cookies。因为这是一个简单 GET
请求,所以浏览器不会对其发起“预检请求”。但是,如果服务器端的响应中未携带 Access-Control-Allow-Credentials
: true
,浏览器将不会把响应内容返回给请求的发送者。
客户端与服务器端交互示例如下:
GET /resources/credentialed-content/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Referer: https://foo.example/examples/credential.html
Origin: https://foo.example
Cookie: pageAccess=2
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
[text/plain payload]
即使第 10 行指定了 Cookie 是属于 https://bar.other
的内容的,但是,如果 https://bar.other
的响应中缺失 Access-Control-Allow-Credentials
: true
(第 16 行),则响应内容会被忽略,不会提供给 web 内容。
预检请求和凭据
CORS 预检请求不能包含凭据。预检请求的响应必须指定 Access-Control-Allow-Credentials: true
来表明可以携带凭据进行实际的请求。
备注: 一些企业认证服务要求在预检请求时发送 TLS 客户端证书,这违反了 Fetch 的规范。
Firefox 87 允许通过在设置中设定 network.cors_preflight.allow_client_cert
为 true
(Firefox bug 1511151)来允许这种不规范的行为。基于 chromium 的浏览器目前总是在 CORS 预检请求中发送 TLS 客户端证书(Chrome bug 775438)。
附带身份凭证的请求与通配符
在响应附带身份凭证的请求时:
- 服务器不能将
Access-Control-Allow-Origin
的值设为通配符“*
”,而应将其设置为特定的域,如:Access-Control-Allow-Origin: https://example.com
。 - 服务器不能将
Access-Control-Allow-Headers
的值设为通配符“*
”,而应将其设置为标头名称的列表,如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
- 服务器不能将
Access-Control-Allow-Methods
的值设为通配符“*
”,而应将其设置为特定请求方法名称的列表,如:Access-Control-Allow-Methods: POST, GET
对于附带身份凭证的请求(通常是 Cookie
),
这是因为请求的标头中携带了 Cookie
信息,如果 Access-Control-Allow-Origin
的值为“*
”,请求将会失败。而将 Access-Control-Allow-Origin
的值设置为 https://example.com
,则请求将成功执行。
另外,响应标头中也携带了 Set-Cookie
字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。
第三方 cookie
注意在 CORS 响应中设置的 cookie 适用一般性第三方 cookie 策略。在上面的例子中,页面是在 foo.example
加载,但是第 19 行的 cookie 是被 bar.other
发送的,如果用户设置其浏览器拒绝所有第三方 cookie,那么将不会被保存。
请求中的 cookie(第 10 行)也可能在正常的第三方 cookie 策略下被阻止。因此,强制执行的 cookie 策略可能会使本节描述的内容无效(阻止你发出任何携带凭据的请求)。
Cookie 策略受 SameSite 属性控制。
其他参考内容
实践排查解决
部署静态页面服务 (端口号为 8888) 及后端接口服务 (端口号为 8080) ,启用不同的端口,不遵循同源策略。
若干访问控制场景
简单请求
静态页面
<!DOCTYPE html>
<html>
<head>
<title>跨域请求</title>
<meta charset="utf-8">
<script type="text/javascript" src="request.js"></script>
</head>
<body>
<div id="container"></div>
<script type="text/javascript">
new Request().send('http://localhost:8080/server/cors',{
success: function(data){
document.write(data)
document
}
});
</script>
</body>
</html>
后端服务
@Controller
@RequestMapping("/server")
public class CorsController {
@RequestMapping(value="/cors", method= RequestMethod.GET)
@ResponseBody
public String ajaxCors(HttpServletRequest request) throws Exception{
return "SUCCESS";
}
}
在不配置跨域过滤器时,请求响应如下:
可见请求头带了 Origin,但响应并没有给出 Access-Control-Allow-Origin。控制台中浏览器输出 Access to XMLHttpRequest at 'http://localhost:8080/server/cors' from origin 'http://localhost:8888' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
在响应体中显示 Fail load response data:No data found for resource with given identifier
。
在增加配置跨域过滤器时,请求响应如下:
可见响应正常返回数据。
注意:
浏览器请求是发出去了的,服务端也会正确返回,但是我们拿不到 response 的内容。但逻辑是执行了的,只是前端拿不到正确返回的数据。
预检请求
简单请求的条件如下:
- HTTP 值含:GET、HEAD、POST
- Content-Type 值含:
text/plain
、multipart/form-data
、application/x-www-form-urlencoded
- 请求头仅含:
Accept
、Accept-Language
、Content-Language
、Content-Type
、DPR
、Downlink
、Save-Data
、Viewport-Width
、Width
不满足上述要求的在发送正式请求前都要先发送一个预检请求,预检请求以 OPTIONS 方法发送,浏览器通过请求方法和请求头能够判断是否发送预检请求。
Client 发送如下请求:
new Request().send('http://localhost:8080/server/options',{
method: 'POST',
body: {
company: '大众点评'
},
header: {
'Content-Type': 'application/json'
},
success: function(data){
document.write(data)
}
});
Server 端处理请求的 Controller:
@Controller
@RequestMapping("/server")
public class CorsController {
@RequestMapping(value="/options", method= RequestMethod.POST)
@ResponseBody
public String options(HttpServletRequest request) throws Exception{
return "SUCCESS";
}
}
因为请求时,请求头中塞入了 Header,'Content-Type': 'application/json'
。根据前面讲述的可以知道,浏览器会以 OPTIONS 方法发出一个预检请求,浏览器会在请求头中加入 Access-Control-Request-Headers:content-type
、Access-Control-Request-Method:POST
。这个预检请求的作用在这里就是告诉服务器:我会在后面请求中以 POST 方法发送 Request Header 带有 Content-Type 的请求,询问服务器是否允许。
在这里服务器还没有做任何允许这种请求的设置,所以浏览器控制台报错 Access to XMLHttpRequest at 'http://localhost:8080/server/options' from origin 'http://localhost:8888' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
。说明了出错的原因: 服务端在预检请求的响应中没有告诉浏览器允许协议头 Content-Type,即服务端需要设置响应头 Access-Control-Allow-Headers,允许浏览器发送带 Content-Type 的请求。
在设置完成后可以看到请求成功。
但是这里有个问题,需要预检请求时,浏览器会发出两次请求,一次 OPTIONS,一次 POST。两次都返回了数据。这样服务端如果逻辑复杂,整体逻辑就会走两遍,浏览器会两次拿到相同的数据,所以服务端的 Filter 可以改一下,如果是 OPTIONS 请求,在设置完跨域请求响应头后就不走后面的逻辑直接返回。
注意:
- 对于 POST 请求设置响应头 Content-Type 为某些值、自定义请求头等情况,浏览器会先以 OPTIONS 方法发送一个预检请求,并设置相应的请求头。
- 服务端还是正常返回,但如果预检请求响应头中不设置相应的响应头,预检请求不通过,不会再发出第二次请求来获取数据。
带凭证信息的请求
浏览器在发送请求时需要给服务端发送 Cookie,服务端根据 Cookie 中的信息做一些身份验证等。默认情况下,浏览器向不同域的发送 Ajax 请求,不会携带发送 Cookie 信息。
Client 请求:
var containerElem = document.getElementById('container')
new Request().send('http://localhost:8080/server/testCookie',{
method: 'POST',
// withCredentials: true,
header: {
'Content-Type': 'application/json'
},
success: function(data){
containerElem.innerHTML = data
}
});
Server 响应:
@RequestMapping(value="/testCookie")
@ResponseBody
public String testCookie(HttpServletRequest request, HttpServletResponse response) throws Exception{
String str = "SUCCESS";
Cookie[] cookies = request.getCookies();
String school = getSchool(cookies);
if(school == null || school.length() == 0){
addCookie(response);
} return str + buildText(cookies);
}
private String buildText(Cookie[] cookies){
String text = "";
if(cookies!=null){
for(int i=0; i<cookies.length;i++){
text = text + new StringBuilder().append("<br/>")
.append(cookies[i].getName())
.append("=")
.append((cookies[i].getValue())).toString();
} } return text;
}
private static String getSchool(Cookie[] cookies) {
String school = "";
if(cookies == null){
return school;
}
int len = cookies.length;
for(int i = 0; i < len; i++) {
Cookie cookie = cookies[i];
if(cookie != null && "school".equals(cookie.getName())) {
school = cookie.getValue();
break;
} } return school;
}
private static void addCookie(HttpServletResponse response) {
int maxAge = -1;
Cookie cookie = new Cookie("school", "seu");
cookie.setDomain("localhost");
cookie.setPath("/");
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}
服务端收到请求,判断 Cookie 中有没有 school,没有就添加 Cookie.
可以看到响应头中有 Set-Cookie,再次请求时,如果是同源请求,浏览器会将 Set-Cookie 中的值放在请求头中,但是对于跨域请求,默认是不发送这个 Cookie 的。如果要让浏览器发送 Cookie,需要在 Client 设置 XMLHttpRequest 的 withCredentials 属性为 true。
设置后,再次请求控制台报错:Access to XMLHttpRequest at 'http://localhost:8080/server/testCookie' from origin 'http://localhost:8888' 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.
。
即需要在后端将 Access-Control-Allow-Credentials
请求头置为 true
。
此时可看到在第二次请求该接口时会带上请求头 Cookie 并包含 school 信息。
附件
request.js
var Request = class Request{
constructor(){
this.method = 'GET'
}
getXMLHttpRequest(){
let xhr = null;
try{
xhr = new XMLHttpRequest();
}catch(e1){
try{
//IE5和IE6
xhr = new ActiveXObject("microsoft.xmlhttp");
}catch(e2){
window.alert("您的浏览器不支持Ajax,换一个浏览器试试吧!");
}
}
return xhr;
}
send(url,opts={}){
let xhr = this.getXMLHttpRequest();
xhr.open(opts.method || this.method,url,opts.asyn || true);
xhr.withCredentials = opts.withCredentials;
xhr.onreadystatechange = function(){
switch(xhr.readyState){
case 0 :
console.log("请求未初始化");
break;
case 1 :
console.log("服务器连接已建立");
break;
case 2 :
console.log("请求已接收");
break;
case 3 :
console.log("请求处理中");
break;
case 4 :
console.log("请求已完成,且响应已就绪");
if(xhr.status >=200 && xhr.status < 300 || xhr == 304){
console.log(xhr.responseText);
opts.success && opts.success.call(xhr,xhr.responseText)
}else{
console.log(`status: ${xhr.status},请求出错`);
}
break;
}
}
opts.header = opts.header || {};
for (var key in opts.header) {
xhr.setRequestHeader(key, opts.header[key]);
}
xhr.send(opts.body);
return this;
}
}
SpringBoot 设置跨域请求配置
CrossOrigin 注解
Controller 层级的配置,并且配置读取的优先级高于 Filter。需要注意的是 origin 的配置需要包含:协议 http
、服务 www
、域名 localhost
、端口 8888
。
@RestController
@RequestMapping("/server")
@CrossOrigin(
origins = {"http://localhost:8888"},
methods = {RequestMethod.GET}
)
public class CorsController {
...
}
Filter 过滤器
请求层级的配置。根据上述介绍的 CORS 协议进行设置即可。
@Component
public class CORSFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse resp = (HttpServletResponse)servletResponse;
resp.setHeader("Access-Control-Allow-Origin", "http://localhost:8888");
resp.setHeader("Access-Control-Allow-Methods", "GET,POST");
resp.setHeader("Access-Control-Allow-Headers", "Content-Type");
resp.setHeader("Access-Control-Allow-Credentials","true");
//OPTION请求就直接返回
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getMethod().equals("OPTIONS")) {
resp.setStatus(200);
resp.flushBuffer();
}else {
filterChain.doFilter(servletRequest,servletResponse);
}
}
public void destroy() {}
}