跨域资源共享CORS实战
CORS是一个W3C标准,全称“跨域资源共享”(Cross-origin resource sharing);CORS定义了一种浏览器和服务器可以交互以确定允许跨院请求是否安全的方式。CORS协议需要浏览器和服务器端同时支持。
CORS协议
W3C的CORS规范将跨域资源请求划分为两种类型,“简单请求”和“非简单请求”。
简单请求
定义
要弄清楚CORS规范将哪些类型的跨域资源请求划分为简单请求的范畴,需要额外理解几个名称的含义:
简单(HTTP)方法
CORS规范将 GET 、HEAD 和 POST 这三个HTTP方法时为 简单HTTP方法;
简单(请求)报头(Simple Header)
CORS规范将请求报头为 Accept、Accept-Language、Content-Language 以及 Content-Type 为(application/x-www-form-urlencoded、multipart/form-data、text-plain )的请求报头称为 简单请求报头;
自定义请求报头(Author Request Header/Custom Request Header)
请求报头包括两种,一种是通脱浏览器自动生成的报头,另外一种是由Javascript程序自动添加的报头(比如调用XMLHttpRequest的setRequestHeader方法可以添加任意报头),后者称为自定义报头。
CORS规范将满足如下条件的的请求定义为简单请求:请求采用 简单HTTP方法 ,并且 自定义请求报头 为空,或者自定义报头 为 简单请求报头。
基本流程
对于简单请求,浏览器直接发出CORS请求。具体来说就是在头信息中增加一个 Origin 字段。如:
1 2 3 4 5 6
| GET /api/operator/v1/get1 HTTP/1.1 Origin: http://api.a.com Host: api.b.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
|
上面的信息头中 Origin 字段用来说明本次请求来自哪个源(协议+域名+端口)。服务器根据这个值决定是否同意这次请求。客户端根据返回请求判断相应头是否包含 Access-Control-Allow-Origin 字段来判断请求是否允许。这种错误无法通过状态码识别,因为HTTP回应的状态码可能是200。
1 2 3 4
| Access-Control-Allow-Origin: http://api.a.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: myHeader Content-Type: text/html; charset=utf-8
|
上面的信息之中 Access-Control 打头的的属性是与CORS请求相关的。
Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时 Origin 的值,要么是一个 *;且不支持通配符;
Access-Control-Allow-Credentials
该字段可选;boolean值,表示是否允许发送Cookie,默认为false。设为True表名服务器明确许可,Cookie可以包含在请求中,一起发送给服务器。
同时开发者也必须在AJAX请求中打开withCredentials属性,如:
1 2
| var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
|
Access-Control-Expose-Headers
该字段可选。CORS请求是,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到6个基本字段(基本相应报文头):Cache-Control 、 Content-Language 、Content-Type、Expires、Last-Modified、Pragma。 如果想拿到其他字段就必须在Access-Control-Expose-Headers里面指定。
非简单请求
对于不是简单请求的跨域请求,则被称为非简单请求;非简单请求的CORS请求,会在正式通信之前增加一次HTTP查询请求,称为“预检”请求。
预检请求
预检请求是一种OPTIONS类型的请求,请求服务器端接口访问权限。下面以访问接口 operator/get为例;
请求报文:
1 2 3 4 5 6 7 8 9 10
| OPTIONS /api/operator/v1/get2 HTTP/1.1 Host: api.b.com Connection: keep-alive Access-Control-Request-Method: GET Origin: http://api.a.com User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 Access-Control-Request-Headers: content-type Accept: */* Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
|
响应报文:
1 2 3 4 5 6 7
| HTTP/1.1 204 Access-Control-Allow-Origin: http://api.a.com Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, OPTIONS Access-Control-Max-Age: 3600 Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With Access-Control-Allow-Credentials: true Date: Fri, 26 Oct 2018 10:16:31 GMT
|
如果浏览器否定了“预检”请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这是,浏览器就会认定服务器不同意预检请求,并因此触发一个错误。
服务器相应的其他CORS相关字段:
Access-Control-Allow-Methods
该字段必须;他是逗号分隔的一个字符串,表名服务器支持的所有跨域方法。注意,返回的是所有支持的方法;
Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Header ,则 Access-Control-Allow-Header字段是必须的。它也是逗号分隔的一个字符串;
Access-Control-Allow-Credentials
与简单请求含义相同;
Access-Control-Max-Age
该字段可选,用来指定背刺预检请求的有效期,单位为“秒”
基本流程
一旦服务器通过了“预检”请求,以后每次浏览器正常CORS请求,和简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。
服务器端实现CORS
WEB容器配置实现
NGINX配置实现CORS
通过Nginx反向代理,对Http请求进行拦截处理以实现服务器端跨域
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| location / { if ($http_origin ~ \.test\.com) { add_header Access-Control-Allow-Origin $http_origin; add_header Access-Control-Allow-Methods GET,POST,OPTIONS; add_header Access-Control-Allow-Credentials true; add_header Access-Control-Allow-Headers DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type; add_header Access-Control-Max-Age 1728000; } if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin $http_origin; add_header Access-Control-Allow-Methods GET,POST,OPTIONS; add_header Access-Control-Allow-Credentials true; add_header Access-Control-Allow-Headers DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type; add_header Access-Control-Max-Age 1728000; return 204; } proxy_set_header Host $host; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Scheme $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://xxx.xxx.xxx.xxx; }
|
- 优点:配置简单,实现便捷快速
- 缺点:配置上稍显麻烦,安全验证上偏弱,不够灵活
SpringMVC 代码实现CORS跨域
Java Servlet Filter实现Cors
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| package com.my.web.filter;
import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils;
import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;
@Component @WebFilter(urlPatterns = "/*") @Slf4j public class CrosFilter implements Filter{
@Override public void init(FilterConfig filterConfig) throws ServletException {
}
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; HttpServletRequest request = (HttpServletRequest) servletRequest;
String origin = request.getHeader("Origin"); if (!StringUtils.isEmpty(origin)) { response.setHeader("Access-Control-Allow-Origin", origin); } response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, OPTIONS"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With"); response.setHeader("Access-Control-Allow-Credentials", "true"); if ("OPTIONS".equals(request.getMethod())) { response.setStatus(HttpStatus.NO_CONTENT.value()); } else { filterChain.doFilter(servletRequest, servletResponse); } }
@Override public void destroy() {
} }
|
- 优点:可以加入自定义逻辑,进行全局配置,也可以进行细微控制
- 缺点:多个拦截器是需要注意拦截器的顺序
SpringMVC addCorsMappings实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| package com.my.web.config;
import com.google.common.base.Splitter; import com.google.common.collect.Iterables; import com.xiwei.marketing.web.interceptor.LoginInterceptor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import javax.annotation.Resource;
@Component public class WebMvcConfig extends WebMvcConfigurationSupport {
@Resource private LoginInterceptor loginInterceptor;
@Value("${privilege.white.request.urls}") private String whiteUrls;
@Value("${cross.origins}") private String crossOrigins;
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns(Iterables.toArray(Splitter.on(',').split(whiteUrls), String.class));
}
@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins(Iterables.toArray(Splitter.on(',').split(crossOrigins), String.class)) .allowedMethods("GET", "HEAD", "POST","PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(86400); } }
|
- 优点:配置优雅,简单
- 缺点:如果程序中有使用拦截器对Cookie进行登录验证时,拦截器的殊勋会在CorsMapping之前,导致认证失败,在使用Cookie进行登录认证时采用了 Filter实现的方式。
@CrossOrigin注解
1 2
| @RequestMapping @CrossOrigin
|
- 优点:可加入到每个具体上API方法上,实现简单
- 缺点:通addCrosmappings全局配置
总结
在进行前后端分离的微服务架构中,在服务器端分别尝试了三种方法,由于需要用到浏览器Cookie,后两种在Cookie支持上没有很好的办法,最终从安全上,可配置上,考虑采用了使用拦截器的方式。