1、跨域的基本概念
a、跨域的解释
要了解跨域,首先需要知晓浏览器的同源策略,简单的说就是两个请求协议、端口、主机都相同,则两个请求具有相同的源,可以自由访问。下表以http://store.company.com/dir/page.html为例,进行一些 url 的同源检测:
浏览器的同源策略控制了不同源之间的交互,也就出现了跨域的问题,对于为什么引入同源策略,官方给出的解释是 “同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。”浏览器的同源策略在一定程度上能规避一些危险、提高攻击的成本。
b、跨域问题的解决
- JSONP: 这种解决跨域问题的方式前后端都需要有改动,在此不做介绍。
- CORS: 一个W3C标准,全程跨域资源共享 (Cross-Origin Resource Sharing),本文也主要讲解该种解决方式。
2、CORS
a、简单请求&非简单请求
浏览器将 CORS 请求分为两类:简单请求和复杂请求,简单请求需要满足以下两种条件:
- 请求方式为 HEAD、GET、POST 这三种方式之一
- HTTP头信息中开发者添加的信息不超过以下几种:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type : 只限于三个值 : application/x-www-form-urlencode、multipart/form-data、text/plain
凡是不同时满足上述两个条件的,就属于复杂请求。浏览器对两种请求的处理方式也是不一样的。
b、简单请求
对于简单请求,浏览器直接发出 CORS 请求,具体来说,就是在头信息之中,添加一个 Origin 字段.
简单请求上图头信息中, Origin 字段用来说明,本次请求来自来个源,服务器根据这个值决定是否同意这个请求。如果该值在许可范围(即允许跨域访问),服务器就会返回一个正常的 HTTP 回应,会多出几个头信息字段:
简单请求 Response上图中的头信息中,有三个与跨域有关的字段,都以 Access-Control- 开头。
- Access-Control-Allow-Origin : 该字段必须的,表示接受该值对应的域名的请求。
- Access-Control-Allow-Credentials : 该值是一个布尔值,表示是否允许发送 COOKIE 。默认情况下, COOKIE 不包括在 CORS 请求之中,设置为 true,即表示服务器明确许可, COOKIE 可以包含中跨域请求中,一起发送给服务器。这个值也只能设置为 true ,如果服务器不要浏览器发送 COOKIE,删除该字段即可。需要注意的是,当前端通过设置 xhr.withCredentials = true 允许携带COOKIE信息时, Access-Control-Allow-Origin 就不能设置为 * ,必须指定明确的、与请求网页一致的域名。同时,COOKIE依然遵循同源策略,只有用服务器域名设置的COOKIE才会上传,其他域名的COOKIE并不会上传。
- Access-Control-Expose-Headers : 该字段可选。 CORS 请求时, XMLHttpRequest 对象的 getResponseHeader() 字段只能拿到6个基本字段: Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma 。如果想要拿到这6个值之外的值,就必须在 Access-Control-Expose-Headers 指定。
c、非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方式是 PUT、DELETE,或者 Content-Type 字段类型是 application/json。非简单请求的 CORS请求,会在正式通信之前,增加一次 HTTP 查询请求,称为预检请求(preflight)。
预检请求预检请求用的请求方法是 OPTIONS。下图是该预检请求对应的正式请求:
预检请求对应正式请求信息3、基于filter的跨域实现
这种实现方式较为简单,判断允许跨域访问后在 Response 头信息中添加 Access-Control-Allow-Origin、Access-Control-Allow-Method等字段信息。代码如下:
public class CorsFilter extends OncePerRequestFilter {private static Logger log &#61; LoggerFactory.getLogger(CorsFilter.class);private static List whiteList &#61; new ArrayList<>();//跨域白名单static {whiteList.add("http://mall.yjc.jd.com");whiteList.add("http://jshopx.jd.com");whiteList.add("http://mall.yao.jd.com");whiteList.add("http://yao-shop.jd.com");}&#64;Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//请求的地址String originUrl &#61; request.getHeader("origin");//查看是否在白名单里面boolean isAllow &#61; whiteList.contains(originUrl);if (isAllow) {response.setHeader("Access-Control-Allow-Origin", originUrl);response.setHeader("Access-Control-Allow-Credentials", "true");response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");response.setHeader("Access-Control-Max-Age", "1800");//30分钟response.setHeader("Access-Control-Allow-Headers", "x-requested-with, content-type");filterChain.doFilter(request, response);}else {//对于非白名单域名的请求&#xff0c;不予进行访问&#xff0c;不然还会进入到controller方法中执行对应的逻辑return;}}
}
4、&#64;CrossOrigin注解
Spring 从4.2版本后开始支持 &#64;CrossOrigin 注解实现跨域&#xff0c;这在一定程度上简化了我们实现跨域访问的开发成本&#xff0c;在需要跨域访问的方法或者类上加上这个注解便大功告成。但在不知晓其原理的情况下使用该注解跨域出了问题将无从下手解决。以下是 &#64;CrossOrigin 的一些基础知识&#xff0c;部分设计到 Spring 框架加载 Bean 的源码。
&#64;CrossOrigin- String[] origins: 允许来源域名的列表&#xff0c;例如 &#39;www.jd.com&#39;&#xff0c;匹配的域名是跨域预请求 Response 头中的 &#39;Access-Control-Aloow_origin&#39; 字段值。不设置确切值时默认支持所有域名跨域访问。
- String[] allowedHeaders: 跨域请求中允许的请求头中的字段类型&#xff0c; 该值对应跨域预请求 Response 头中的 &#39;Access-Control-Allow-Headers&#39; 字段值。 不设置确切值默认支持所有的header字段&#xff08;Cache-Controller、Content-Language、Content-Type、Expires、Last-Modified、Pragma&#xff09;跨域访问。
- String[] exposedHeaders: 跨域请求请求头中允许携带的除Cache-Controller、Content-Language、Content-Type、Expires、Last-Modified、Pragma这六个基本字段之外的其他字段信息&#xff0c;对应的是跨域请求 Response 头中的 &#39;Access-control-Expose-Headers&#39;字段值。
- RequestMethod[] methods: 跨域HTTP请求中支持的HTTP请求类型&#xff08;GET、POST...&#xff09;&#xff0c;不指定确切值时默认与 Controller 方法中的 methods 字段保持一致。
- String allowCredentials: 该值对应的是是跨域请求 Response 头中的 &#39;Access-Control-Allow-Credentials&#39; 字段值。浏览器是否将本域名下的 COOKIE 信息携带至跨域服务器中。默认携带至跨域服务器中&#xff0c;但要实现 COOKIE 共享还需要前端在 AJAX 请求中打开 withCredentials 属性。
- long maxAge: 该值对应的是是跨域请求 Response 头中的 &#39;Access-Control-Max-Age&#39; 字段值&#xff0c;表示预检请求响应的缓存持续的最大时间&#xff0c;目的是减少浏览器预检请求/响应交互的数量。默认值1800s。设置了该值后&#xff0c;浏览器将在设置值的时间段内对该跨域请求不再发起预请求。
Spring对支持跨域访问的请求所做的操作&#xff1a;
图1、DefaultCorsProcessor#handlerInternal设置response的header图2、DefaultCorsProcessor#processRequest处理请求&#xff0c;判断是否支持跨域问题: 图2中&#xff0c;判断是否需要往 response 头中 set 相关值的依据是 config &#61;&#61; null&#xff0c;这里不禁好奇&#xff0c;CorsConfiguration 有何作用&#xff1f;在哪里初始化&#xff1f;以下将揭晓。
图3、CorsConfiguration看到这些属性是不是有点眼熟&#xff0c;对的&#xff0c;就是对应 &#64;CrossOrigin 注解的字段信息。
CorsConfiguration 的初始化过程涉及到 Spring 对 bean 的加载过程&#xff0c;以下为一些关键步骤的截图。
图4、AbstractAutowireCapableBeanFactory#invokeInitMethods初始化bean图5、AbstractHandlerMethodMapping#initHandlerMethods 判断bean是否有&#64;Controller等注解图6、关键步骤&#xff0c;AbstractHandlerMethodMapping#register图7、根据注解信息初始化CorsConfiguration, RequestMappingHandlerMapping#initCorsConfiguration处理跨域/非跨域请求的过程&#xff1a;
DispatcherServlet#doService() --> DispatcherServlet#doDispatch() --> HttpRequestHandlerAdapter#handle() --> PreFlightHandler#handleRequest() --> DefaultCorsProcessor#processRequest() --> DefaultCorsProcessor#handleInternal()
5、总结&#xff1a;
a、注解方式与过滤器方式适用场景&#xff1a;
过滤器方式适合于大范围的控制跨域&#xff0c;比如某个controller类的所有放大全部支持某个或几个具体的域名跨域访问的情形。而对于细粒度的跨域控制&#xff0c;比如一个 controller 类中 methodA 支持域名 originA 跨域访问&#xff0c; methodB 支持域名 originB 跨域访问的情况&#xff0c;当然过滤器方式也能实现&#xff0c;但适用注解的方式能轻松很多&#xff0c;尤其是上述情况比较多的情形。
b、&#64;CrossOrigin 注解是基于拦截器还是过滤器实现的&#xff1f;
答案是都不是。由图6内容可知&#xff0c;在初始化 bean 的时候有在一个 ConcurrentHashMap 中保存 url 与与其对应的跨域对象 CorsConfiguration &#xff08;该 url 不支持跨域访问时该对象为空&#xff09;&#xff0c;图2 在处理请求的时候会 get url对应的 CorsConfiguration 对象对象不为空的情况下&#xff0c;执行 handleInternal 为 Response 头添加对应的字段信息。这个过程是在 DispatcherServlet 执行 doService() 方法过程中进行的&#xff0c;并不涉及到过滤器或者拦截器。虽然有一个 CorsFilter 的类&#xff0c;但 debug 发现&#xff0c;并不会经过这个过滤器。
c、适用过程中遇到的问题&#xff1a;
在正确配置注解或者添加过滤器的情况下&#xff0c;仍然提示跨域失败。后来排查问题是由于项目未完全前后端分离且登陆拦截器对于需要登陆而未登陆的请求直接重定向到登陆页&#xff0c;这就导致跨域访问一些需要经过登陆拦截器的接口时预请求被重定向至登陆页而跨域失败。处理方式也很简单&#xff0c;放开登陆拦截&#xff0c;在接口中进行判断并返回相应的实体对象。