跨域访问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头信息来标识允许来自特定的域的跨域访问。跨域请求诸如<img>标签加载来自不同域的图片、引用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)

预检请求会在真正的请求之前发送一次预检的OPTIONS请求,先校验最终的请求是否可以安全发送。满足以下任一条件即是一个预检请求:

  • 请求方法是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对象

预检请求示例,实际发生了两次请求,一次options检验请求,一次是实际获取数据的请求:

预检请求示例

CORS中使用到的关键响应头信息

  • Access-Control-Allow-Origin

    该头部项的值可配置为通配符:*,表示允许来自任何域的跨域访问

    也可指定具体的域,比如:http://domain.a.com

    注:跨域请求中的请求头部信息中的Origin为请求所在域,与该响应头部值匹配即可完成跨域访问

  • Access-Control-Allow-Methods

    该头部项指定了跨域请求中允许使用的请求方法,也可配置成通配符*,多个值用逗号分隔,如:

    Access-Control-Allow-Methods: GET, POST, OPTIONS, HEAD, PUT
    
  • Access-Control-Allow-Headers

    该头部项指定了跨域请求中允许使用的头部信息,由于请求中经常使用到的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,如果仅仅是前端网页在请求时指定withCredentialstrue,那后端返回的结果将被浏览器忽略,从而请求无法完成。跨域请求默认不发送cookie,前端网页请求时,将withCredentials值设为true,表示允许发送cookie信息,当然也需要服务器明确许可。

    Access-Control-Allow-Credentials: true
    

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{
		String origin = request.getHeader("Origin");
		List<String> allowOrigins = CORSUtil.getAllowOrigins();
		if("OPTIONS".equalsIgnoreCase(request.getMethod) ||
			(StringUtils.isNotBlank(origin) && allowOrigins.contains(origin)){
			// 当为options请求 或 当前域是允许跨域访问的域 都设置必要的 完成跨域访问的响应头部信息
			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");
		}
	}
	
	@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>

参考链接:

http://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

http://www.ruanyifeng.com/blog/2016/04/cors.html

猜你喜欢

转载自my.oschina.net/u/861950/blog/1818842