1.发现问题
在开发基于WEB应用的时候,一般情况下前端负责界面展示,后端负责业务逻辑,尤其是当前分布式、微服务、前后端分离架构的广泛使用,前端调用后端获取数据,难免会出现数据访问跨域现象:
当浏览器console出现下面的提示,说明出现了跨域问题。
Access to XMLHttpRequest at 'http://www.float.com/goods/findPage?page=1&size=5' from origin 'www.integer.net.cn' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
关键词:
origin:域、源
CORS:全称是"跨域资源共享"(Cross-Origin Resource Sharing)
blocked:阻止,阻塞
Access-Control-Allow-Origin:HTTP协议中与跨域有关的请求头信息。
CORS policy:CORS策略
接下来有个问题,域是什么鬼?
2.什么是域
当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为不同的域
3.为什么会出现跨域问题
出于安全考虑,浏览器使用同源策略(SameOriginPolicy)进行访问的限制。
所谓同源(或同一个域)就是两个请求具有相同的协议(protocol),主机(host)和端口号(port),这三者均相同则表示这两个请求同源。
同源策略防止当前域的javascript脚本和另外一个域的内容进行交互,这就是为什么出现跨域访问被阻止的原因。
同源策略是Web应用程序安全模型中的一个重要概念,它由 Netscape 公司在1995年引入浏览器。该策略最初是为保护DOM的访问而设计的,Web浏览器允许第一个Web页面中包含的脚本访问第二个Web页面中的数据,但前提是两个Web页面具有相同的源(origin)。后来进行了扩展,此策略可防止一个页面上的恶意脚本(ajax请求)访问另一个网页上的敏感数据。
恶意脚本访问敏感数据
目前Web应用程序广泛依赖HTTP cookie来维护经过身份验证的用户会话,服务器基于cookie信息来显示或者获取敏感信息。客户端必须保证无关站点与当前站点严格分离,以防止数据机密性或完整性丢失。
同源策略主要防止一个域的javascript脚本和另外一个域的内容进行交互。
主要限制包括:
不同源的页面之间Cookie、LocalStorage 和 IndexDB无法共享
不同源AJAX 请求不能发送
通过相应的HTML标记嵌入的资源不受限制,如图像,CSS和脚本
图像:
CSS:
脚本:
(1)当访问http://www.integer.net.cn的时候,浏览器发送GET 请求获取主页,这时候当前页面的域指定为http://www.integer.net.cn
(2)通过访问1.png图片,没有跨域,允许访问,通过访问2.png图片,虽然跨域,但访问的是HTML嵌入资源,不受同源策略影响,依然可以访问。
(3)通过访问a.css,没有跨域,允许访问,通过< link href="http://www.double.net.cn/b.css">访问b.css,虽然跨域,但访问的是HTML嵌入资源,不受同源策略影响。
(4)使用ajax异步请求http://www.integer.net.cn域下的数据,没有跨域,允许访问
(5)使用ajax异步请求http://www.float.com域下的数据,此时跨域,同源策略阻止继续访问,是否允许访问由CORS策略控制。
从以上分析可以看出,只有当ajax异步请求跨域时,同源策略才会起作用,跨域请求就一定不能访问呢?当然不是,上面已经提及,是否能访问跨域资源由CORS策略决定的,那么什么是CORS策略呢?
4.CORS跨域解决方案(服务端)
CORS策略,它允许浏览器向跨域服务器发出XMLHttpRequest(AJAX)请求,从而克服了AJAX只能同源访问的限制。
整个CORS通信过程,前端都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨域,就会自动添加一些附加的头信息(针对简单请求),有时还会多出一次附加的请求(针对复杂请求),但用户不会有感觉。因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨域通信。
上面这段话,翻译成大白话就是浏览器发现AJAX请求跨域,添加附加信息(简单请求)或者附加请求(复杂请求)发送到服务器,如果服务器端允许当前访问,则继续访问返回响应结果,如果服务器不允许该来源访问,则会出现跨域访问现象。所以,主动权掌握在服务器手里。这就是CORS策略。
就像你想要约隔壁班的女生看电影,人家和你不是一个班根本不认识你,去不去由人家决定。当然,附加的信息就是你表明自己身份去邀请人家,这里还有一个问题,如果只是看电影这种简单请求,你拿着票表达诚意就行。但是对于其他复杂请求,问一次肯定不行,先要进行一次预请求,试探一下,试探成功后再进行复杂请求。
那么哪些请求是简单请求,哪些是复杂请求呢?
简单请求需要满足以下几点:
请求是GET、POST、HEAD三种
HTTP的头信息不超出以下几种字段 Accept、Accept-Language、Content-Language、Content-Type
Content-Type 的值仅限于下列三者之一:text/plain、multipart/form-data、application/x-www-form-urlencoded
什么是复杂请求呢,除了简单请求都是复杂请求。
比如说你需要发送PUT、DELETE等HTTP请求,或者发送Content-Type: application/json的内容,这些就都是复杂请求。
那么简单请求中附加的信息是什么呢?
跨域请求HTTP头当中要求包含一个域(Origin)的信息。该域包含协议名、地址以及一个可选的端口。不过这一项实际上由浏览器代为发送,开发者不需要操作,这就是简单请求附加的信息。
服务器端对简单请求进行响应,部分响应头及解释如下:
Access-Control-Allow-Origin(必含) - 不可省略,否则请求按失败处理。该项控制数据的可见范围,如果希望数据对任何人都可见,可以填写"*"。
Access-Control-Allow-Credentials(可选) - 该项标志着请求当中是否包含cookies信息,只有一个可选值:true(必为小写)。如果不包含cookies,请略去该项,而不是填写false。这一项与XmlHttpRequest对象(ajax请求)当中的withCredentials属性应保持一致,即withCredentials为true时该项也为true;withCredentials为false时,省略该项不写。反之则导致请求失败。
简单请求
任何不满足上述简单请求要求的请求,都属于复杂请求。
复杂请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。预检请求为OPTIONS请求,用于向服务器请求权限信息的。预检请求被成功响应后,才会发出真实请求,携带真实数据。
复杂请求
预请求以OPTIONS形式发送,当中同样包含域,并且还包含了两项CORS特有的内容:
Access-Control-Request-Method – 该项内容是实际请求的种类,可以是GET、POST之类的简单请求,也可以是PUT、DELETE等等。
Access-Control-Request-Headers – 该项是一个以逗号分隔的列表,当中是复杂请求所使用的头部。
显而易见,这个预请求实际上就是在为之后的实际请求发送一个权限请求,在预回应返回的内容当中,服务端应当对这两项进行回复,以让浏览器确定请求是否能够成功完成。
复杂请求的部分响应头及解释如下:
Access-Control-Allow-Origin(必含) – 和简单请求一样的,必须包含一个域。
Access-Control-Allow-Methods(必含) – 这是对预请求当中Access-Control-Request-Method的回复,这一回复将是一个以逗号分隔的列表。尽管客户端或许只请求某一方法,但服务端仍然可以返回所有允许的方法,以便客户端将其缓存。
Access-Control-Allow-Headers(当预请求中包含Access-Control-Request-Headers时必须包含) – 这是对预请求当中Access-Control-Request-Headers的回复,和上面一样是以逗号分隔的列表,可以返回所有支持的头部。这里在实际使用中有遇到,所有支持的头部一时可能不能完全写出来,而又不想在这一层做过多的判断,没关系,事实上通过request的header可以直接取到Access-Control-Request-Headers,直接把对应的value设置到Access-Control-Allow-Headers即可。
Access-Control-Allow-Credentials(可选) – 和简单请求当中作用相同。
Access-Control-Max-Age(可选) – 以秒为单位的缓存时间。预请求的的发送并非免费午餐,允许时应当尽可能缓存。
一旦预回应如期而至,所请求的权限也都已满足,则实际请求开始发送。
5.Spring CORS –@CrossOrigin注解
Spring MVC 提供@CrossOrigin 注解. 该注解可以标注在类或者方法上
5.1. Spring CORS 配置
@CrossOrigin 默认将允许所有的源,所有头信息,@RequestMapping标注的方法进行跨域访问,maxAge 为30分钟。
CrossOrigin 配置参数
5.2. @CrossOrigin 注解标注在Controller类
@CrossOrigin(origins ="*", allowedHeaders ="*")
@Controller
publicclassHomeController{
@GetMapping(path="/")
public String homeInit(Model model) {
return"home";
}
}
表明该类的所有方法,均实现CORS策略。
5.3. @CrossOrigin 注解标注在方法
@Controller
publicclassHomeController{
@CrossOrigin(origins ="*", allowedHeaders ="*")
@GetMapping(path="/")
public String homeInit(Model model) {
return"home";
}
}
表明该方法实现CORS策略
5.4. @CrossOrigin 在方法级覆盖类级注解
@Controller
@CrossOrigin(origins ="*", allowedHeaders ="*")
public class HomeController{
@CrossOrigin(origins ="http://example.com")
@GetMapping(path="/")
publicString homeInit(Model model) {
return"home";
}
}
homeInit() 方法只能被域 http://example.com访问,HomeController 的其他方法可以被所有的域
6.Spring CORS – 全局配置
6.1. 实现WebMvcConfigurer
为了让整个应该所有的请求均实现CORS跨域方案, 可以使用WebMvcConfigurer 并添加到 CorsRegistry中。
import org.springframework.context.annotation.Configuration;
import rg.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
publicclass CorsConfiguration implements WebMvcConfigurer{
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "POST");
}
}
6.2. WebMvcConfigurer Bean
spring boot应用中, 推荐声明一个 WebMvcConfigurer bean.
@Configuration
publicclassCorsConfiguration{
@Bean
publicWebMvcConfigurercorsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry){
registry.addMapping("/**").allowedOrigins("http://localhost:8080");
}
};
}
}
6.3 CORS 与Spring Security
为了能够使CORS 支持 Spring security, 配置CorsConfigurationSource bean 并且使用 HttpSecurity.cors() 进行设置。
@EnableWebSecurity
publicclassWebSecurityConfigextendsWebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http)throwsException{
http.cors().and()//other config
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
7.Java CORS过滤器
如果应用中没有集成Spring或者SpringBoot框架,可以直接使用过滤器来进行配置,基本原理就是对Http 头信息就行设置。
7.1Response Headers
· Access-Control-Allow-Origin : 指示允许哪些域进行跨域访问. “*” 代表没有限制.
· Access-Control-Allow-Credentials : 指示跨域访问是否支持用户身份,即cookie.
· Access-Control-Expose-Headers : 指示哪些头信息暴露出来.
· Access-Control-Max-Age : 指示预请求响应在客户端缓存的时间.
· Access-Control-Allow-Methods : 指示允许哪些方法可以访问资源
· Access-Control-Allow-Headers :指示在实际请求中哪些头信息被允许.
7.2. Request Headers
· Origin : 指示跨域请求或者预请求的来源.
· Access-Control-Request-Method : 在预请求中使用,告诉服务器实际请求讲使用该方法
· Access-Control-Request-Headers :在预请求中使用,告诉服务器实际请求中将带的头信息.
7.3. Java CORS 过滤器demo
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.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Servlet Filter implementation class CORSFilter
*/
// Enable it for Servlet 3.x implementations
/* @ WebFilter(asyncSupported = true, urlPatterns = { "/*" }) */
public class CORSFilter implements Filter {
/**
* Default constructor.
*/
public CORSFilter() {
// TODO Auto-generated constructor stub
}
/**
* @see Filter#destroy()
*/
public void destroy() {
// TODO Auto-generated method stub
}
/**
* @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
*/
public void doFilter(ServletRequest servletRequest,ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
System.out.println("CORSFilter HTTP Request: " + request.getMethod());
// Authorize (allow) all domains to consume the content
((HttpServletResponse) servletResponse).addHeader("Access-Control-Allow-Origin", "*");
((HttpServletResponse) servletResponse)
.addHeader("Access-Control-Allow-Methods","GET, OPTIONS, HEAD, PUT, POST");
HttpServletResponse resp = (HttpServletResponse) servletResponse;
// For HTTP OPTIONS verb/method reply with ACCEPTED status code -- per CORS handshake
if (request.getMethod().equals("OPTIONS")) {
resp.setStatus(HttpServletResponse.SC_ACCEPTED);
return;
}
// pass the request along the filter chain
chain.doFilter(request, servletResponse);
}
/**
* @see Filter#init(FilterConfig)
*/
public void init(FilterConfig fConfig) throws ServletException {
// TODO Auto-generated method stub
}
}
web.xml配置
CorsFilter
com.howtodoinjava.examples.cors.CORSFilter
CorsFilter
/*