跨域问题详解

一直都没有时间捣鼓博客,发现自己的描述和总结的习惯都没有了,换新电脑有三个月了,觉得这段时间过得很快,事实上我不更博客都不止三个月了,换电脑是一个原因,其他理由就是毕业季,转正,同事的离职,两个版本的老项目快速迭代,的的确确没有太多时间给自己总结,说白了就是没有时间摸鱼了,晚上有时还要加班,周末都用来睡觉和打游戏虚度,老电脑也不在身边,总之感觉自己过得浑浑噩噩,一边补洞一边自己写出新的烂代码来。对了,另一个不更的原因是CTO成天坐旁边,不明白为啥他总不爱坐自己的办公室😥(好在在写这篇文章的时候他已经换了一个位置坐)

接下来会列一些这么长时间里遇到的比较经典的问题,由于上班节奏变快了,有些忘了记录下来,但之后的日子我还是会继续恢复以前的习惯,常总结,今天梳理一下工作中遇到n次的跨域问题!

第一次遇到跨域还是在去年冬天刚实习的时候,前端大佬说他那调我的http接口报403,捣鼓半天问了组里的大哥找到项目里有应对跨域问题的拦截器,最后问题原因出在前端大佬把访问的路径写岔了,跨域问题也就不了了之。

什么是跨域

在JavaScript中,有一个很重要的安全性限制,被称为“Same-Origin Policy”(同源策略)。这一策略对于JavaScript代码能够访问的页面内容做了很重要的限制,即JavaScript只能访问与包含它的文档在同一域下的内容。

JavaScript这个安全策略在进行多iframe或多窗口编程、以及Ajax编程时显得尤为重要。根据这个策略,在baidu.com下的页面中包含的JavaScript代码,不能访问在google.com域名下的页面内容;甚至不同的子域名之间的页面也不能通过JavaScript代码互相访问。对于Ajax的影响在于,通过XMLHttpRequest实现的Ajax请求,不能向不同的域提交请求,例如,在abc.example.com下的页面,不能向def.example.com提交Ajax请求,等等。

以上来自https://www.cnblogs.com/smiler/p/5829621.html

所谓同源是指,域名,协议,端口相同。当页面在执行一个脚本时会检查访问的资源是否同源,如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。

这个问题表现上来说与后端并没有什么关系,浏览器并不能在同一个js请求不同域名的接口,否则就会跨域,所以大多数接口需要后端转发,变成同一个域名和端口才能给前端请求,但有的时候明明是同一个项目甚至同一个Controller下的接口也会出现跨域问题,这就不知道前端大佬们到底是如何请求的了,可能是都写在了一块吧。

解决方法

最常用的解决方法是CORS,者是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing),定义了一种浏览器和服务器交互的方式来确定是否允许跨域请求。 它是一个妥协,有更大的灵活性,但比起简单地允许所有这些的要求来说更加安全。

目前我了解到的有两种实现方法,一是加处理响应头的过滤器或拦截器,二是SpringMVC的一个注解@CrossOrigin,第二种方法比较偷懒,我们先来介绍这种偷懒的方法。

@CrossOrigin

在Controller中的方法上添加一个@CrossOrigin注解:

1
2
3
4
5
@CrossOrigin
@GetMapping("/{id}")
public JSONObject abababa(@RequestParam String id) {
// ...
}

有2个参数:

origins : 允许可访问的域列表

maxAge:准备响应前的缓存持续的最大时间(以秒为单位)。

还可以加上CORS的设置:

1
@CrossOrigin(allowCredentials = "true", allowedHeaders = "*")

这虽然是一个非常简单的处理方法,但有的时候加了还是没有用,看网上有许多分析问题原因究竟出在哪里的帖子,有时要加上CORS配置才生效,有时要制定@RequestMapping是POST还是GET,有时加了这些都没用,把注解搬到类上也没用,感觉有的时候不应该一概而论的解决,要看浏览器究竟报了什么错,根据错误来判断究竟要加什么头或条件,我比较喜欢用土方法,写个过滤器,

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
public class CROSFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {

HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
// response.setHeader("Access-Control-Allow-Headers", "epid,version,web_platform");
response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));

chain.doFilter(req, res);

}

@Override
public void destroy() {

}
}

这个例子是由于出现了Access-Control-Allow-Headers缺省导致的错误,所以一开始我加了浏览器报错上提示的请求头,”epid,version,web_platform”是某个项目里需要的请求头,缺省就报405,与前端大佬沟通后发现他们请求的时候头就会带这些东西,于是后来遇到类似情况,要改Access-Control-Allow-Headers,我都会优先从请求头里取。

这里再解释一下其他几个CORS相关的参数:

(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

这个参数的缺省带来的报错非常常见,通常写成*就行,但也遇到写了也没用的情况,那么要具体问题具体分析,看到底缺了什么

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

(4)Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。

前端大佬表示他们在请求时通常都会预检,发一个OPTIONS请求,所以在上面的例子中我也加了这个

(5)Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。

(6)Access-Control-Allow-Credentials

该字段与简单请求时的含义相同。

(7)Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

总之,要与前端大佬沟通好是缺省什么,哪个参数报的错,一般他们请求过来什么,我们给他们加什么就行,切忌所有请求一概而论的胡乱加*,当然也许@CrossOrigin也可以达到一样的效果,只是由于我比较笨拙喜欢复杂的写法

WebMvcConfigurer(SpringBoot)

另外,如果你用的是一个SpringBoot项目,不妨试试WebMvcConfigurer,(SpringBoot2.x版本为WebMvcConfigurerAdapter

建一个Configuration实现WebMvcConfigurer ,老的WebMvcConfigurerAdapter有解决跨域的方法,十分强大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** 解决跨域问题 **/
public void addCorsMappings(CorsRegistry registry) ;
/** 添加拦截器 **/
void addInterceptors(InterceptorRegistry registry);
/** 这里配置视图解析器 **/
void configureViewResolvers(ViewResolverRegistry registry);
/** 配置内容裁决的一些选项 **/
void configureContentNegotiation(ContentNegotiationConfigurer configurer);
/** 视图跳转控制器 **/
void addViewControllers(ViewControllerRegistry registry);
/** 静态资源处理 **/
void addResourceHandlers(ResourceHandlerRegistry registry);
/** 默认静态资源处理器 **/
void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer);

例如你可以这样解决跨域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableWebMvc
public class WebAppConfig extends WebMvcConfigurerAdapter{

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
.allowedHeaders("Content-Type", "X-Requested-With", "accept", "Origin", "Access-Control-Request-Method",
"Access-Control-Request-Headers")
.maxAge(3600)
.allowCredentials(true);
}
}

高版本的SpringBoot可以这样实现:

1
2
3
4
@Configuration
public class MvcConfigure implements WebMvcConfigurer{
//同上
}