Spring Boot 实现跨域的 5 种方式,看看哪种适合? ()
什么是跨域问题
跨域指的是浏览器在执行网页中的 JavaScript 代码时,由于浏览器同源策略SOP(Same origin policy,是一种约定)的限制,只能访问同源(协议、域名、端口号均相同)的资源,而不能访问其他源(协议、域名、端口号任意一个不同)的资源。
浏览器只是针对同源策略的一种实现。
Tips:跨域限制是浏览器同源策略的限制, 如果直接在服务端请求,是不会触发跨域限制的。
所限制的跨域交互包括:
- 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
- 无法接触非同源网页的 DOM
- 无法向非同源地址发送 AJAX 请求
下表给出了与 URL http:///dir/page.html 的源进行对比的示例:
| URL | 结果 | 原因 |
|---|---|---|
| http:///dir2/other.html | 同源 | 只有路径不同 |
| https:///secure.html | 非同源 | 协议不同 |
| http://:81/dir/etc.html | 非同源 | 端口号不同 |
| http:///dir/other.html | 非同源 | 主机名不同 |
如下图,从http://localhost:63342/站点页面中向ttp://localhost:8080/chat21/cors/test2发送一个ajax请求,则出现了红色的错误信息,错误中包含了Access-Controll-Allow-Origin这样字样的错误,以后看到这个的时候,大家就要一眼看出来这是跨域问题。

为什么存在同源策略限制
我们通过Ajax发送跨域请求,虽然用户体验提高了,但是也有潜在的威胁存在,常见的就是CSRF(Cross-site request forgery)跨站请求伪造。跨站请求伪造也被称为one-click attack 或者 session riding,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法,举个例子:
假如一家银行用以运行转账操作的URL地址如下:
http://icbc.com/aa?bb=cc,那么,一个恶意攻击者可以在另一个网站上放置如下代码:<img src="http://icbc.com/aa?bb=cc">,如果用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会遭受损失。
Ajax 为什么不能跨域
Ajax 其实就是向服务器发送一个 GET 或 POST 请求,然后取得服务器响应结果,返回客户端。Ajax 跨域请求,在服务器端不会有任何问题,只是服务端响应数据返回给浏览器的时候,浏览器根据响应头的Access-Control-Allow-Origin字段的值来判断是否有权限获取数据。
因此,服务端如果没有设置跨域字段设置,跨域是没有权限访问,数据被浏览器给拦截了。
所以,要解决的问题是:如何从客户端拿到返回的数据?
其实,在同源策略的基础上,选择性地为同源策略开放了一些后门。例如 img、script、style 等标签,都允许跨域引用资源。
所以, JSONP 来了。
JSONP
JSONP(JSON with Padding(填充))是 JSON 的一种“使用模式”,本质不是 Ajax 请求,是 script 标签请求。
JSONP 请求本质上是利用了 “Ajax 请求会受到同源策略限制,而 script 标签请求不会” 这一点来绕过同源策略。
JSONP 缺点
- 只支持 GET 请求
- 只支持跨域 HTTP 请求这种情况,不能解决不同域的两个页面之间如何进行 JavaScript 调用的问题
- 调用失败的时候不会返回各种 HTTP 状态码。
- 安全性,万一假如提供 JSONP 的服务存在页面注入漏洞,即它返回的 javascript 的内容被人控制的。
跨域资源共享 CORS
跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。
允许在下列场景中使用跨域 HTTP 请求:
- 由
XMLHttpRequest或Fetch发起的跨域 HTTP 请求 - Web 字体 (CSS 中通过
@font-face使用跨域字体资源) WebGL贴图- 使用 drawImage 将 Images/video 画面绘制到
canvas
简单请求、非简单请求
浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求(不会触发 CORS 预检请求)。
- 请求方法是以下三种方法之一:
HEAD、GET、POST - HTTP 的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type(只限于三个值):
application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同时满足上面两个条件,就属于非简单请求。
CORS 如何工作
首先,浏览器判断请求是简单请求还是复杂请求(非简单请求)。
如果是复杂请求,那么在进行真正的请求之前,浏览器会先使用 OPTIONS 方法发送一个预检请求 (preflight request),OPTIONS 是 HTTP/1.1 协议中定义的方法,用以从服务器获取更多信息。
该方法不会对服务器资源产生影响,预检请求中同时携带了下面两个首部字段:
Access-Control-Request-Method: 这个字段表明了请求的方法;Access-Control-Request-Headers: 这个字段表明了这个请求的 Headers;Origin: 这个字段表明了请求发出的域。
服务端收到请求后,会以 Access-Control-* response headers 的形式对客户端进行回复:
Access-Control-Allow-Origin: 能够被允许发出这个请求的域名,也可以使用*来表明允许所有域名;Access-Control-Allow-Methods: 用逗号分隔的被允许的请求方法的列表;Access-Control-Allow-Headers: 用逗号分隔的被允许的请求头部字段的列表;Access-Control-Max-Age: 这个预检请求能被缓存的最长时间,在缓存时间内,同一个请求不会再次发出预检请求。
简单请求
对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,自动增加一个 Origin 字段,用来说明请求来自哪个源。服务器拿到请求之后,在回应时对应地添加Access-Control-Allow-Origin字段,如果 Origin 不在这个字段的范围中,那么浏览器就会将响应拦截。
Access-Control-Allow-Credentials。这个字段是一个布尔值,表示是否允许发送 Cookie,对于跨域请求,浏览器对这个字段默认值设为 false,而如果需要拿到浏览器的 Cookie,需要添加这个响应头并设为 true, 并且在前端也需要设置withCredentials属性:
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
Access-Control-Expose-Headers。这个字段是给 XMLHttpRequest 对象赋能,让它不仅可以拿到基本的 6 个响应头字段(包括Cache-Control、Content-Language、Content-Type、Expires、Last-Modified和Pragma), 还能拿到这个字段声明的响应头字段。比如这样设置:
Access-Control-Expose-Headers: aaa
那么在前端可以通过 XMLHttpRequest.getResponseHeader('aaa') 拿到 aaa 这个字段的值。
非简单请求
非简单请求相对而言会有些不同,体现在两个方面: 预检请求和响应字段。
预检请求
比如使用 PUT 请求方法,这个预检请求的请求行和请求体是下面这个格式:
OPTIONS / HTTP/1.1
Host: 127.0.0.1:8000
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: x-custom-header
Origin: http://127.0.0.1:8001
预检请求的方法是OPTIONS,同时会加上 Origin 源地址和 Host 目标地址,这很简单。同时也会加上两个关键的字段:
Access-Control-Request-Method, 列出 CORS 请求用到哪个 HTTP 方法Access-Control-Request-Headers,指定 CORS 请求将要加上什么请求头
这是预检请求。接下来是响应字段。
响应字段也分为两部分,一部分是对于预检请求的响应,一部分是对于CORS 请求的响应。
预检请求的响应:
HTTP/1.1 200 OK
Content-Type: text/json
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: PUT, POST, GET
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 2000
Access-Control-Allow-Credentials: true
Date: Fri, 27 Mar 2020 08:16:58 GMT
Connection: keep-alive
Transfer-Encoding: chunked
在预检请求的响应返回后,如果请求不满足响应头的条件,则触发XMLHttpRequest的onerror方法,当然后面真正的 CORS 请求也不会发出去了。
CORS 请求的响应:现在它和简单请求的情况是一样的。浏览器自动加上 Origin 字段,服务端响应头返回 Access-Control-Allow-Origin。在设置的Access-Control-Max-Age: 2000里是不会再次发送预检请求的,除非时间过期。
Java后端实现 CORS 跨域请求的方式
对于 CORS的跨域请求,主要有以下几种方式可供选择:
- 返回新的CorsFilter
- 重写 WebMvcConfigurer
- 使用注解 @CrossOrigin
- 手动设置响应头 (HttpServletResponse)
- 自定web filter 实现跨域
注意:
- CorFilter / WebMvConfigurer / @CrossOrigin 需要 SpringMVC 4.2以上版本才支持,对应Spring Boot 1.3版本以上
- 上面前两种方式属于全局 CORS 配置,后两种属于局部 CORS配置。如果使用了局部跨域是会覆盖全局跨域的规则,所以可以通过 @CrossOrigin 注解来进行细粒度更高的跨域资源控制
- 其实无论哪种方案,最终目的都是修改响应头,向响应头中添加浏览器所要求的数据,进而实现跨域
1.返回新的 CorsFilter(全局跨域)
在任意配置类,返回一个 新的 CorsFIlter Bean ,并添加映射路径和具体的CORS配置路径。
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
//1. 添加 CORS配置信息
CorsConfiguration config = new CorsConfiguration();
//放行哪些原始域
config.addAllowedOrigin("*");
//是否发送 Cookie
config.setAllowCredentials(true);
//放行哪些请求方式
config.addAllowedMethod("*");
//放行哪些原始请求头部信息
config.addAllowedHeader("*");
//暴露哪些头部信息
config.addExposedHeader("*");
//2. 添加映射路径
UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**",config);
//3. 返回新的CorsFilter
return new CorsFilter(corsConfigurationSource);
}
}
2. 重写 WebMvcConfigurer(全局跨域)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
//是否发送Cookie
.allowCredentials(true)
//放行哪些原始域
.allowedOrigins("*")
.allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"})
.allowedHeaders("*")
.exposedHeaders("*");
}
}
3. 使用注解 (局部跨域)
在控制器(类上)上使用注解 @CrossOrigin:,表示该类的所有方法允许跨域。
@RestController
@CrossOrigin(origins = "*")
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "hello world";
}
}
在方法上使用注解 @CrossOrigin:
@RequestMapping("/hello")
@CrossOrigin(origins = "*")
// @CrossOrigin(value = "http://localhost:8081")
// 指定具体ip允许跨域
public String hello() {
return "hello world";
}
4. 手动设置响应头(局部跨域)
使用 HttpServletResponse 对象添加响应头(Access-Control-Allow-Origin)来授权原始域,这里 Origin的值也可以设置为 “*”,表示全部放行。
@RequestMapping("/index")
public String index(HttpServletResponse response) {
response.addHeader("Access-Allow-Control-Origin","*");
return "index";
}
5. 使用自定义filter实现跨域
首先编写一个过滤器,可以起名字为:MyCorsFilter.java
package com.mesnac.aop;
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.http.HttpServletResponse;
import org.springframework.stereotype.Component;
@Component
public class MyCorsFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,content-type");
chain.doFilter(req, res);
}
public void init(FilterConfig filterConfig) {}
public void destroy() {}
}
在web.xml中配置这个过滤器,使其生效
<!-- 跨域访问 START-->
<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>com.mesnac.aop.MyCorsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 跨域访问 END -->
















