跨域访问CORS探究
转载于:九层塔炒薄壳
什么是跨域?
跨域,简单地讲,就是一个Web应用(http://www.a.com)下的文档或脚本访问另一个Web应用(http://www.b.com)下的资源。任何两个应用所在域,只要存在协议、域名或端口任意一个不相同,即被认为访问是跨域的。
为什么会出现跨域访问限制?
由于浏览器同源策略,我们这里主要讨论XmlHttpRequest同源策略,XmlHttpRequest同源策略禁止XHR对象向不同源的服务器地址发送请求,这是浏览器出于安全考虑所做的限制。
使用ajax向另一个域下的应用发送一个请求,在服务端未做跨域相关的处理之前将报如下错误:
Failed to load http://localhost:8081/api: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3200' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
什么是CORS?
CORS,全称Cross-Origin Resource Sharing,即跨域(源)资源共享。CORS使用目标服务器上返回的HTTP头信息来标识允许来自特定的域的跨域访问。跨域请求诸如标签加载来自不同域的图片、引用CDN的脚本样式等是允许的。然而为了安全考虑,浏览器显示了从脚本里发起的跨域HTTP请求,由于XMLHttpRequest和Fetch API遵从同源策略,在没有服务器端返回允许跨域的CORS头部信息时,这种类型的请求将被限制。
CORS机制保障了浏览器和服务器之间跨域请求和数据传输的安全性,使得XMLHttpRequest和Fetch API进行跨域访问有了可能。
两种类型的跨域请求
简单请求 (Simple requests)
简单请求是指当前跨域请求不触发“跨域中的的预检验”(即后面说到的预检请求),简单请求需要满足下面所有条件:
- 请求方法是GET、POST或HEAD三者之一
- 请求头部信息中仅允许出现以下列表的请求头
Accept
Accept-Language
Content-Language
Content-Type,并且该值为application/x-www-form-urlencoded、multipart/form-data、text/plain三者之一
Last-Event-ID
DPR
Save-Data
Viewport-Width
Width - XMLHttpRequestUpload对象在请求中没有注册事件监听
- 在请求中没有使用ReadableStream对象
预检请求 (Preflight requests)、
跨域资源共享(CORS)标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。
预检请求会在真正的请求之前发送一次预检的OPTIONS请求,这次"预检"请求会带上头部信息 Access-Control-Request-Headers: Content-Type:
OPTIONS /api/test HTTP/1.1
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
... 省略了一些
先校验最终的请求是否可以安全发送。满足以下任一条件即是一个预检请求:
- 请求方法是PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH之一
- 请求头信息包含除了Accept、Accept-Language、Content-Language、Last-Event-ID、DPR、Save-Data、Viewport-Width、Width之外的任何头信息
- 请求头信息包含Content-Type,其值不为application/x-www-form-urlencoded、multipart/form-data、text/plain三者任何一个
- 在XMLHttpRequestUpload对象中使用事件监听
- 请求中使用了ReadableStream对象
服务器回应时,返回的头部信息如果不包含Access-Control-Allow-Headers: Content-Type则表示不接受非默认的的Content-Type。即出现以下错误:
Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response
CORS中使用到的关键响应头信息
Access-Control-Allow-Origin
该头部项的值可配置为通配符:*,表示允许来自任何域的跨域访问
也可指定具体的域,比如:http://domain.a.com
注:跨域请求中的请求头部信息中的Origin为请求所在域,与该响应头部值匹配即可完成跨域访问
Access-Control-Allow-Methods
防止出现以下错误:
Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.
该头部项指定了跨域请求中允许使用的请求方法,也可配置成通配符*,多个值用逗号分隔,如:
Access-Control-Allow-Methods: GET, POST, OPTIONS, HEAD, PUT
Access-Control-Allow-Headers
防止出现以下错误:
Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response.
这个错误表示当前请求Content-Type的值不被支持。其实是我们发起了"application/json"的类型请求导致的。这里涉及到一个概念:预检请求(preflight request),请看上面"预检请求"的介绍。
该头部项指定了跨域请求中允许使用的头部信息,由于请求中经常使用到的Content-Type不为application/x-www-form-urlencoded、multipart/form-data、text/plain时,请求将转为预检请求,通常地,需要将Content-Type、其他一些常用的头部和自定义的头部信息在此处指定,以便跨域访问正常完成。
Access-Control-Allow-Credentials
当前端网页请求指定了withCredentials为true时,后端返回响应头中需要指定Access-Control-Allow-Credentials值为true,如果仅仅是前端网页在请求时指定withCredentials为true,那后端返回的结果将被浏览器忽略,从而请求无法完成。跨域请求默认不发送cookie,前端网页请求时,将withCredentials值设为true,表示允许发送cookie信息,当然也需要服务器明确许可。
示例,nginx服务配置文件设置允许跨域
location / {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
给OPTIONS 添加 204的返回,是为了处理在发送POST请求时Nginx依然拒绝访问的错误
CORS解决跨域访问限制的实现
如果想让我们的后端应用允许某些特定域的跨域请求,一般地,我们需要在拦截请求处对请求进行校验并对允许的跨域请求响应设置适当的响应头部信息。
典型地,项目中使用了Servlet统一拦截了请求,这个时候需要实现我们对应允许的请求方法,如doGet、doPost处理普通的GET/POST请求,doOptions处理预检请求。
public class AppServlet extends HttpServlet{
@Override
public void init() throws ServletException{
super.init();
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
doPost(request, response);
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
String origin = request.getHeader("Origin");
List<String> allowOrigins = CORSUtil.getAllowOrigins();
if(StringUtils.isNotBlank(origin) && allowOrigins.contains(origin)){
// 校验当前域是允许跨域访问的域
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "*");
response.setHeader("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With");
}
// compose your response here
}
/**
* 处理跨域中的OPTIONS预检请求,OPTIONS请求同样需要指定允许访问的域
*/
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doOptions(req, resp);
String origin = req.getHeader("Origin");
if(StringUtils.isNotBlank(origin)){
resp.setStatus(HttpStatus.SC_NO_CONTENT);
//允许预检请求跨域,此处让所有OPTIONS请求都能跨域,实际检验在post中进行
resp.setHeader("Access-Control-Allow-Origin", origin);
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Methods", "*");
resp.setHeader("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With");
}
}
}
或者使用过滤器Filter一站式处理所有类型的请求:
public class CorsFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException{
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException{
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;
String origin = httpRequest.getHeader("Origin");
List<String> allowOrigins = new ArrayList<>();
allowOrigins.add("http://localhost:3201");
if("OPTIONS".equalsIgnoreCase(httpRequest.getMethod()) ||
(StringUtils.isNotBlank(origin) && allowOrigins.contains(origin))){
httpResponse.setHeader("Access-Control-Allow-Origin", origin);
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Methods", "*");
httpResponse.setHeader("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With");
}
chain.doFilter(request, response);
}
@Override
public void destroy(){
}
}
web.xml配置:
<filter>
<filter-name>corsFilter</filter-name>
<filter-class>com.test.filter.CorsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>corsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>