CORS背后的基本思想是使用自定义的HTTP头部允许浏览器和服务器相互了解对方,从而决定请求或响应成功与否.
Access-Control-Allow-Origin:指定授权访问的域
Access-Control-Allow-Methods:授权请求的方法(GET, POST, PUT, DELETE,OPTIONS等)
一:简单的自定义CORSFilter / Interceptor
适合设置单一的(或全部)授权访问域,所有配置都是固定的,特简单。也没根据请求的类型做不同的处理
在web.xml 中添加filter
<filter>
<filter-name>cros</filter-name>
<filter-class>cn.ifengkou.test.filter.CORSFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>cros</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
新增CORSFilter 类
@Component
public class CORSFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
response.addHeader("Access-Control-Allow-Origin", "*");
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
response.addHeader("Access-Control-Allow-Headers", "Content-Type");
response.addHeader("Access-Control-Max-Age", "1800");//30 min
filterChain.doFilter(request, response);
}
}
Access-Control-Allow-Origin只能配置 或者一个域名*
比如配置了192.168.56.130,那么只有192.168.56.130 能拿到数据,否则全部报403异常
response.addHeader("Access-Control-Allow-Origin", "http://192.168.56.130");
二:Nginx 配置支持Ajax跨域
这里是一个nginx启用COSR的参考配置:来源
#
# Wide-open CORS config for nginx
#
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}
}
三:支持多域名配置的CORS Filter
因为知道已经有可以用的库可以解决,所以就没重复造轮子了。其实因为懒,看看别人的源码算了。。。
在mvnrepository搜索cors-filter,目前也就两个可以用
- org.ebaysf.web 的 cors-filter,项目地址:https://github.com/ebay/cors-filter
- com.thetransactioncompany的 cors-filter,项目地址:http://software.dzhuvinov.com/cors-filter.html
这两个也都大同小异,因为ebay开源在github上,也有详细的README,那么就以ebay的cors-filter为例
配置
添加依赖包到项目:
<dependency>
<groupId>org.ebaysf.web</groupId>
<artifactId>cors-filter</artifactId>
<version>1.0.1</version>
</dependency>
添加配置(具体配置项,还是见项目的README.md吧)
<filter>
<filter-name>CORS Filter</filter-name>
<filter-class>org.ebaysf.web.cors.CORSFilter</filter-class>
<init-param>
<param-name>cors.allowed.origins</param-name>
<param-value>http://192.168.56.129,http://192.168.56.130</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.methods</param-name>
<param-value>GET,POST,HEAD,OPTIONS,PUT</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.headers</param-name>
<param-value>Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CORS Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
源码分析
源码地址:github。但通过IDEA Decompiled 出来的更清晰.....,以下是反编译的
ebaysf的cors-filter 只有一个类CORSFilter。也就是一个拦截器,implements Filter
public final class CORSFilter implements Filter {
通过是实现Filter 的init 方法从配置文件中读取参数:
public void init(FilterConfig filterConfig) throws ServletException {
this.parseAndStore("*", "GET,POST,HEAD,OPTIONS", "Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers", "", "true", "1800", "false", "true");
this.filterConfig = filterConfig;
this.loggingEnabled = false;
if(filterConfig != null) {
String configAllowedOrigins = filterConfig.getInitParameter("cors.allowed.origins");
String configAllowedHttpMethods = filterConfig.getInitParameter("cors.allowed.methods");
String configAllowedHttpHeaders = filterConfig.getInitParameter("cors.allowed.headers");
String configExposedHeaders = filterConfig.getInitParameter("cors.exposed.headers");
String configSupportsCredentials = filterConfig.getInitParameter("cors.support.credentials");
String configPreflightMaxAge = filterConfig.getInitParameter("cors.preflight.maxage");
String configLoggingEnabled = filterConfig.getInitParameter("cors.logging.enabled");
String configDecorateRequest = filterConfig.getInitParameter("cors.request.decorate");
this.parseAndStore(configAllowedOrigins, configAllowedHttpMethods, configAllowedHttpHeaders, configExposedHeaders, configSupportsCredentials, configPreflightMaxAge, configLoggingEnabled, configDecorateRequest);
}
}
parseAndStore 方法,解析参数。以 解析cors.allowed.orgins为例;其他参数同理
Set e;
if(allowedOrigins != null) {
if(allowedOrigins.trim().equals("*")) {
this.anyOriginAllowed = true;
} else {
this.anyOriginAllowed = false;
e = this.parseStringToSet(allowedOrigins);
this.allowedOrigins.clear();
this.allowedOrigins.addAll(e);
}
}
//parseStringToSet
//对多域名用点分割,加到HashSet中,再赋给allowedOrigins(Collection<String> allowedOrigins = new HashSet();)
private Set<String> parseStringToSet(String data) {
String[] splits;
if(data != null && data.length() > 0) {
splits = data.split(",");
} else {
splits = new String[0];
}
HashSet set = new HashSet();
if(splits.length > 0) {
String[] arr$ = splits;
int len$ = splits.length;
for(int i$ = 0; i$ < len$; ++i$) {
String split = arr$[i$];
set.add(split.trim());
}
}
return set;
}
如何实现 doFilter
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if(servletRequest instanceof HttpServletRequest && servletResponse instanceof HttpServletResponse) {
HttpServletRequest request1 = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
//识别request 属于哪种类别
CORSFilter.CORSRequestType requestType = this.checkRequestType(request1);
if(this.decorateRequest) {
decorateCORSProperties(request1, requestType);
}
switch(CORSFilter.SyntheticClass_1.$SwitchMap$org$ebaysf$web$cors$CORSFilter$CORSRequestType[requestType.ordinal()]) {
case 1:
this.handleSimpleCORS(request1, response, filterChain);
break;
case 2:
this.handleSimpleCORS(request1, response, filterChain);
break;
case 3:
this.handlePreflightCORS(request1, response, filterChain);
break;
case 4:
this.handleNonCORS(request1, response, filterChain);
break;
default:
this.handleInvalidCORS(request1, response, filterChain);
}
} else {
String request = "CORS doesn\'t support non-HTTP request or response.";
throw new ServletException(request);
}
}
判断request类别,根据类别进行差异化处理。handleSimpleCORS 处理过程,判断是否设置允许所有origin参数,判断是否符合httpMethods要求,判断此次request的origin(origin = request.getHeader("Origin"))是否在allowedOrigins(origin白名单)内。如果在,就设置response.addHeader("Access-Control-Allow-Origin", origin);这样也就实现了多域名支持。流程图就不画了...
public void handleSimpleCORS(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
CORSFilter.CORSRequestType requestType = this.checkRequestType(request);
String origin;
if(requestType != CORSFilter.CORSRequestType.SIMPLE && requestType != CORSFilter.CORSRequestType.ACTUAL) {
origin = "Expects a HttpServletRequest object of type " + CORSFilter.CORSRequestType.SIMPLE + " or " + CORSFilter.CORSRequestType.ACTUAL;
throw new IllegalArgumentException(origin);
} else {
origin = request.getHeader("Origin");
String method = request.getMethod();
if(!this.isOriginAllowed(origin)) {
this.handleInvalidCORS(request, response, filterChain);
} else if(!this.allowedHttpMethods.contains(method)) {
this.handleInvalidCORS(request, response, filterChain);
} else {
if(this.anyOriginAllowed && !this.supportsCredentials) {
response.addHeader("Access-Control-Allow-Origin", "*");
} else {
response.addHeader("Access-Control-Allow-Origin", origin);
}
if(this.supportsCredentials) {
response.addHeader("Access-Control-Allow-Credentials", "true");
}
if(this.exposedHeaders != null && this.exposedHeaders.size() > 0) {
String exposedHeadersString = join(this.exposedHeaders, ",");
response.addHeader("Access-Control-Expose-Headers", exposedHeadersString);
}
filterChain.doFilter(request, response);
}
}
}
为了避免对参数一知半解,就把作者的参数描述表贴上来,通过参数表可以了解下header里面各个参数的作用
param-name | description |
---|---|
cors.allowed.origins | A list of origins that are allowed to access the resource. A '' can be specified to enable access to resource from any origin. Otherwise, a whitelist of comma separated origins can be provided. Ex: http://www.w3.org, https://www.apache.org. Defaults: (Any origin is allowed to access the resource). |
cors.allowed.methods | A comma separated list of HTTP methods that can be used to access the resource, using cross-origin requests. These are the methods which will also be included as part of 'Access-Control-Allow-Methods' header in a pre-flight response. Ex: GET,POST. Defaults: GET,POST,HEAD,OPTIONS |
cors.allowed.headers | A comma separated list of request headers that can be used when making an actual request. These header will also be returned as part of 'Access-Control-Allow-Headers' header in a pre-flight response. Ex: Origin,Accept. Defaults: Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers |
cors.exposed.headers | A comma separated list of headers other than the simple response headers that browsers are allowed to access. These are the headers which will also be included as part of 'Access-Control-Expose-Headers' header in the pre-flight response. Ex: X-CUSTOM-HEADER-PING,X-CUSTOM-HEADER-PONG. Default: None |
cors.preflight.maxage | The amount of seconds, browser is allowed to cache the result of the pre-flight request. This will be included as part of 'Access-Control-Max-Age' header in the pre-flight response. A negative value will prevent CORS Filter from adding this response header from pre-flight response. Defaults: 1800 |
cors.support.credentials | A flag that indicates whether the resource supports user credentials. This flag is exposed as part of 'Access-Control-Allow-Credentials' header in a pre-flight response. It helps browser determine whether or not an actual request can be made using credentials. Defaults: true |
cors.logging.enabled | A flag to control logging to container logs. Defaults: false |
cors.request.decorate | A flag to control if the request should be decorated or not. Defaults: true |
测试:
1.服务端准备接口(我的地址是:http://192.168.10.61:8080/api)
@RequestMapping(method = RequestMethod.GET,value = "test")
@ResponseBody
public HashMap<String,Object> getArticles(){
HashMap<String,Object> map = new HashMap<>();
map.put("result","success");
return map;
}
2.过滤器配置(web.xml),配置允许访问的域为:http://192.168.56.129,http://www.website2.com
<filter>
<filter-name>CORS Filter</filter-name>
<filter-class>org.ebaysf.web.cors.CORSFilter</filter-class>
<init-param>
<param-name>cors.allowed.origins</param-name>
<param-value>http://192.168.56.129,http://www.website2.com</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CORS Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3.准备测试网页index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>cors test page</title>
<script src="jquery.min.js"></script>
<script>
function loadData(){
$.ajax({
url: "http://192.168.10.61:8080/api",
type:"GET",
dataType:"json",
timeout:10000,
success:function(data){
$("#result").append(data.result+"<br />");
console.log(data);
},
error:function(e){
$("#result").append(e.statusText+"<br />");
}
});
}
$(function(){
$("#host").append("origin:"+window.location.origin);
});
</script>
</head>
<body>
<button onclick="loadData()">onclick</button>
<div id="host"></div>
<div id="result" style="height:200px;width:100%"></div>
</body>
</html>
4.将index.html发布到nginx(nginx后面也有方案)
index.html 不能直接用浏览器打开运行,虽然可以调用Ajax请求,但是域是file:///path/index.html
虚拟机增加一个网卡地址(原机器IP是192.168.56.129)
ifconfig eth0:0 192.168.56.130
建立两个测试网站
cd home
mkdir /website1 #站点目录
mkdir /website2
将index.html 传输到这两个目录
配置nginx,增加两个server节点
# ----server1 ----
server {
listen 192.168.56.129:80;
server_name www.website1.com;
location / {
root /website1;
index index.html index.htm;
}
}
# ----server2 ----
server {
listen 192.168.56.130:80;
server_name www.website2.com;
location / {
root /website2;
index index.html index.htm;
}
}
重启nginx服务
./nginx -s reload
5.修改本地hosts文件
//hosts文件路径:windows系统一般在C:\Windows\System32\drivers\etc
192.168.56.129 www.website1.com
192.168.56.130 www.website2.com
通过增加虚拟网卡 、nginx代理 和 修改hosts文件,我在本地就有4个网站(域)可以进行测试了,分别是:
6.测试
准备:
(chrome)打开4个tab,分别进入到上述四个网站,页面打印了当前origin,通过onclick调用Ajax请求,页面布局如下
预期:
- http://192.168.56.129 SUCCESS
- http://192.168.56.130 ERROR
- http://www.website1.com ERROR
- http://www.website2.com SUCCESS
结果:
符合预期!
建议使用,除了对域的过滤,还做了其他很多操作,比简单的自定义过滤器考虑得周全,例如
this.handlePreflightCORS(request1, response, filterChain);
this.handleNonCORS(request1, response, filterChain);
this.handleInvalidCORS(request1, response, filterChain);
总结
cors在开发WebService、RESTful API 时经常会遇到,在以前可能直接通过jsonp解决,jsonp怎样怎样就不多说了。 总之,CORS技术规范出来这么久了,如果不考虑IE6 IE7的问题,那么还是积极拥抱CORS吧
上文三种解决方案,通过搜索引擎均能找到,但估计大部分都是用的第一种最简单的无脑的Cors Filter处理,第二种方案是通过nginx配置的,并不适合所有Web应用。第三种,考虑得很周全,而且使用方便,如果不考虑造重复轮子,推荐使用。
本文所用的测试工程代码太简单了,就不放github了,直接下载吧,项目下载地址
断断续续写了好几天,转载请附带原文路径:http://www.cnblogs.com/sloong/p/cors.html