ajax跨域访问控制:CORS
本文由 小茗同学 发表于 2016-11-07 浏览(2373)
最后修改 2017-11-10 标签:cors ajax 跨域 nginx java

关于跨域

前言

做前端的经常会接触一个名词:跨域,那何为跨域?为什么要跨域?

同源策略

出于安全考虑,浏览器会限制脚本中发起的跨域请求,比如,使用 XMLHttpRequest 对象发起 HTTP 请求就必须遵守同源策略。 具体而言,默认情况下Web 应用程序能且只能使用 XMLHttpRequest 对象向其加载的源域名发起 HTTP 请求,而不能向任何其它域名发起请求。

跨域

只要protocol(如http/https/file)、域名、端口三者有一个不一样即视为跨域。如www.aaa.comwww.aaa.com:81

CORS

虽然浏览器的初衷是好的,为了安全,但是这样也大大限制了web的自由,鉴于此,又有了CORS机制。CORS全称Cross-Origin Resource Sharing,中文含义为跨域资源共享。其原理就是服务器响应请求时返回一个特殊的header,这个header控制哪些网站可以跨域请求自己。

常见的一般有以下几个:

Access-Control-Allow-Origin: http://www.test.com
Access-Control-Allow-Headers: Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With
Access-Control-Allow-Methods: POST, GET, OPTIONS

很多时候浏览器会先发送一个 OPTIONS 请求给目的站点,来查明这个跨域请求对于目的站点是不是安全可接受的(当然也不是全部,比如GET请求就不会)。

需要特别之处的是,WebFont也有同源策略,所以如果跨域了的话也要添加如上header。

关于CORS,还有很多知识点,这里不作详细介绍,有兴趣的可以看HTTP访问控制(CORS)

CORS跨域访问时默认不发送cookie和http认证,如果要把Cookie发到服务器,那么需要:

  1. 服务端返回Access-Control-Allow-Credentials:trueheader;
  2. Access-Control-Allow-Origin不能为*
  3. js需要手动打开withCredentials属性:

原生JS开启跨域携带cookie示例:

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('GET', 'http://www.test.com/test_cors');
xhr.onreadystatechange= function()
{
	if(xhr.readyState == 4) console.log(xhr.responseText)
}
xhr.send();

jQuery写法:

$.ajax({
	url: 'xxx',
	xhrFields: {
		withCredentials: true
	}
});

fetch写法:

fetch('http://www.test.com/test_cors', {credentials: 'include'})
	.then(resp => resp.json())
	.then(json => console.log(json));

credentials开启时Access-Control-Allow-Origin不能设置为*,否则Chrome控制台会报如下错误:

A wildcard '*' cannot be used in the 'Access-Control-Allow-Origin' header when the credentials flag is true.

当然,这个没啥意义,因为真想设置为*的话,服务端可以根据当前请求域名动态设置Origin,以nginx为例(关键是这个$http_origin):

server {
	listen	   80;
	server_name  www.test.com;
	location / {
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header Host $http_host;
		add_header 'Access-Control-Allow-Origin' $http_origin;
		add_header 'Access-Control-Allow-Headers' 'Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With, __session_token';
		add_header 'Access-Control-Allow-Methods' 'POST, GET, OPTIONS';
		add_header 'Access-Control-Allow-Credentials' 'true';
		proxy_pass http://127.0.0.1:8080;
	}
}

非简单请求

//TODO 待完善

几种服务端的实现

Java实现

一般是用过滤器实现:

package com.test.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 解决ajax跨域问题,一般现代浏览器都支持CORS,IE要求IE9+,安卓的webview也支持
 * @date 20160114
 * @author LXA
 */
public class CorsFilter implements Filter
{
	// 允许跨域调用的网站,指定具体网站时只能填一个,所有网站时直接用*表示,开发时为了方便可以设置为*,上线后为了安全建议改成指定的地址
	private String allow;

	@Override
	public void destroy()
	{
		System.out.println("CORS过滤器被销毁!");
	}

	@Override
	public void doFilter(ServletRequest req, ServletResponse rsp, FilterChain filter) throws IOException, ServletException
	{
		HttpServletRequest request = (HttpServletRequest)req;
		HttpServletResponse response = (HttpServletResponse)rsp;
		response.setHeader("Access-Control-Allow-Origin", allow);
		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");
		response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
		filter.doFilter(request, response);
	}

	@Override
	public void init(FilterConfig filterConfig) throws ServletException
	{
		System.out.println("初始化CORS过滤器!");
		allow = filterConfig.getInitParameter("allow");
		if(allow == null || "".equals(allow)) allow = "*";
	}
}

然后在web.xml中配置如下:

<!-- 支持ajax跨域的过滤器 -->
<filter>
	<filter-name>CorsFilter</filter-name>
	<filter-class>com.test.filter.CorsFilter</filter-class>
	<init-param>
		<param-name>allow</param-name>
		<!-- 允许跨域调用的网站,指定具体网站时只能填一个,所有网站时直接用*表示,开发时为了方便可以设置为*,上线后为了安全建议改成指定的地址 -->
		<param-value>*</param-value>
	</init-param>
</filter>
<filter-mapping>
	<filter-name>CorsFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

Nginx实现

server {
	listen	   80;
	server_name  localhost;
	location / {
		add_header 'Access-Control-Allow-Origin' '*';
		add_header 'Access-Control-Allow-Headers' 'Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With';
		add_header 'Access-Control-Allow-Methods' 'POST, GET, OPTIONS';
		root html;
		index  index.html index.htm;
	}
}

apache实现

打开\apache\conf/httpd.conf,搜索<Directory />,然后添加如下3个Header,重启apache生效:

<Directory />
	AllowOverride none
	Require all denied
	Header set Access-Control-Allow-Origin "*"
	Header set Access-Control-Allow-Headers "Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With"
	Header set Access-Control-Allow-Methods "POST, GET, OPTIONS"
</Directory>

fiddler配置实现

使用fiddler下的Willow插件实现:

然后在最下面那个输入框添加Headers设置,冒号分隔,不需要引号,逐个添加:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With

参考

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS