减少跨域中的OPTIONS请求

简介

这篇是继 跨域和OPTIONS这对欢喜冤家 后的一篇文章,在本篇中我们继续探索跨域中的 OPTIONS 请求,主要分享一下:

  • SpringBoot 中除了 CrossOrigin 注解外还有哪些方式可以解决跨域问题?

  • 如何使用 SpringBoot 结合 CORS 减少 OPTIONS 请求?

阅读下面内容之前,强烈建议先阅读之前的 跨域和OPTIONS这对欢喜冤家 这篇文章。

文中使用的代码都可以在 Github 找到,大家根据需要自行采纳。

用上 Nginx

这次使用 Nginx 作为 Web 容器,在本地将HTML跑起来,上次是在 Chrome 中直接打开 HTML 文件的方式来验证跨域问题的。我们知道只要端口不同也会造成跨域问题,那么只需要在 Nginx 中配置一个端口不同于服务端口的虚拟主机就可以达到目的了。

由于我是在 Win10 上面做的例子,包括 Nginx 也是 Windows 版本的,如果没有安装的小伙伴请去 下载Nginx 直接解压即可。

我把 Nginx 解压放到:D:\portable\nginx-1.15.12 这个目录,你可以解压到你认为比较合适的地方。

打开 Nginx 的配置文件 nginx.conf 即在 安装目录\conf 下面,增加一个虚拟主机配置项,修改完成后保存配置文件即可。

1
2
3
4
5
6
7
8
9
10
11
# 自定义虚拟主机,可以同时配置多个虚拟主机
server {
listen 8082; # 不同于服务端口
server_name localhost;
location / {
# 文件路径,注意路径分隔符是 `/` 不是 `\`
root E:/examples/cors-options;
# 默认页面
index index.html index.htm;
}
}

注意:E:/examples/cors-options; 是我的 HTML 文件(options.html )路径,你要根据自己实际HTML路径来配置这个选项,否则后面无法打开该文件。

这里我把这个文件命名为 options.html,HTML文件内容大致如下:

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
<html>
<head>
<meta charset="utf-8">
<title>options-demo</title>
<script type="text/javascript" src="./jquery-3.4.1.min.js"></script>
<script type="text/javascript" src="./jquery.json.js"></script>
<script>
function getReq() {
var url = "http://localhost:8080/signin/name?username=jack&userpwd=123";
$.ajax({
url: url,
type: 'GET',
dataType: 'json',
contentType: 'application/json',
headers: {
token: "yu7rX98xxxx_iii^ddd",
userId: 123,
openid: 231232
}
}).done(function (result) {
console.log("success");
console.log(result);
}).fail(function () {
console.log("error");
})
}
</script>
</head>
<body>
<button onclick="getReq()">用户名登录-GET</button>
</html>

现在启动 Nginx,启动 Nginx 很简单,打开 Windows 终端或者 Git 的终端(如果你安装了 Git 的话,即使没有安装我也强烈建议你安装,因为太好用了),然后 cd到 Nginx 的安装目录。

1
start nginx.exe

打开 Chrome 浏览器,输入下面的网址进行访问:

1
http://localhost:8082/options.html

不出意外的话,可以看到显示一个按钮的视图,顺便把 Chrome 的审查视图(Ctrl+Shift+i)打开,大概是下面截图的样子。

指定域名列表

Nginx 配置完成,文件也可以正常的打开,那我们就来试试是否跟预想的一致?

SpringBoot 关键示例代码如下:

1
2
3
4
5
6
7
8
@RestController
@RequestMapping(value = "signin") // 注意这里不要在signin前后加"/"
public class MSSigninController {
@RequestMapping(value = "/name", method = RequestMethod.GET)
public MSResponse sigin(@RequestParam(value = "username") String userName, @RequestParam(value = "userpwd") String userPwd) {
// 省略
}
}

启动 SpringBoot 服务(默认运行在8080端口不同于网页运行端口8082)完成后,点击 Chrome 视图中的按钮进行请求,截图如下:

可以看到跟我们预期一致,的确造成了跨域请求,并且进行了 OPTIONS 请求。

在 SpringBoot 的某个方法上面添加 CrossOrigin 注解可以解决跨域问题,并且可以指定域名列表,示例代码如下:

1
2
3
4
5
6
7
8
public class MSSigninController {
// 指定域名列表
@CrossOrigin(origins = {"http://localhost:8082"})
@RequestMapping(value = "/name", method = RequestMethod.GET)
public MSResponse sigin(@RequestParam(value = "username") String userName, @RequestParam(value = "userpwd") String userPwd) {
// 省略
}
}

重启服务后,再去验证一下发现请求就可以通过了。

如果你感兴趣,可以修改一下 CrossOrigin 注解中的域名列表端口号,再去请求就会失败。

使用 CrossOrigin 注解指定域名列表,可以从更小的粒度上面控制跨域请求。

那么除了使用 CrossOrigin 注解意外,在 SpringBoot 中还可以怎样解决跨域问题呢?

解决跨域的其他计策

目前除了使用 CrossOrigin 注解,还可以使用下面几种方法来解决跨域问题。

1、自定义Filter

自定义 Filter 可以解决跨域问题,示例代码如下:

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
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@Component
public class MSCorsFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
if ("OPTIONS".equals(request.getMethod())) {
System.out.println("HTTP's OPTIONS Coming");
}
HttpServletResponse response = (HttpServletResponse) res;
// 设置所有的请求域名都可以
response.setHeader("Access-Control-Allow-Origin", "*");
// 设置允许的请求方法
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
// 设置缓存时间单位为秒,在改时间内不需要再发送预检验请求,即缓存该结果
// 设置为0就相当于不设置缓存,即每次都会有OPTIONS请求
response.setHeader("Access-Control-Max-Age", "0");
// 设置允许跨域请求包含content-type头
response.setHeader("Access-Control-Allow-Headers", "*");
System.out.println("Filter has been used.");
chain.doFilter(req, res);
}
public void init(FilterConfig filterConfig) {
}
public void destroy() {
}
}

这里我们是通过设置 Access-Control-Allow-Origin 允许所有的域名(通配符*)都可以访问,如下:

1
response.setHeader("Access-Control-Allow-Origin", "*");

也可以设置指定域名才可以,示例如下:

1
response.setHeader("Access-Control-Allow-Origin", "http://localhost:8082");

那我们需要思考一下了,setHeader 方法只能设置一个指定的域名,如果我想设置多个域名怎么办?

首先告诉你通过下面的方式肯定不行,如下:

1
2
response.addHeader("Access-Control-Allow-Origin", "http://localhost:8082");
response.addHeader("Access-Control-Allow-Origin", "http://localhost:8083");

具体原因大家可以看一下源码就秒懂了。

有一个解决方案,把可以通过跨域访问的域名做成数组也是大家在业务上面经常说的白名单,示例如下:

1
2
3
4
5
6
7
8
// 设置多个域名支持,类似白名单
String[] allowDomain = {"http://localhost:8082", "http://localhost:8083", "http://localhost:8085", "http://localhost:8087"};
Set allowedOrigins = new HashSet(Arrays.asList(allowDomain));
String originHeader = ((HttpServletRequest) req).getHeader("Origin");
System.out.println("originHeader: " + originHeader);
if (allowedOrigins.contains(originHeader)) {
response.setHeader("Access-Control-Allow-Origin", originHeader);
}

再来思考一个问题,上面那种方式自定义Filter会对所有URL即全局的请求都起作用了,能否对指定URL进行过滤呢?

做过Spring的同学肯定知道,我们可以设置 WebFilter,示例如下:

1
@WebFilter(urlPatterns = { "/signin/name" })

这样修改后,需要修改一下SpringBoot相关的代码,首先去掉 Component 注解,示例如下:

1
2
3
4
//@Component
@WebFilter(urlPatterns = { "/signin/name" })
public class MSCorsFilter implements Filter {
}

然后需要在 Application 中添加 ServletComponentScan 注解,示例如下:

1
2
3
4
@ServletComponentScan
@SpringBootApplication
public class SpringbootApplication {
}

结合 Filter 我们可以做出更细粒度更多功能来解决和控制跨域问题。Filter 这种方式对跨域的 GET 和 POST 请求都是支持的。

2、WebMvcConfigurationSupport

在 SpringBoot 中还可以自定义配置来解决跨域问题,通过继承 WebMvcConfigurationSupport 配置 CROS,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class MSCorsConfig extends WebMvcConfigurationSupport {
@Override
protected void addCorsMappings(CorsRegistry registry) {
super.addCorsMappings(registry);
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}

还可以指定 URL 和域名,修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class MSCorsConfig extends WebMvcConfigurationSupport {
@Override
protected void addCorsMappings(CorsRegistry registry) {
super.addCorsMappings(registry);
registry.addMapping("/signin/name")
.allowedOrigins("http://localhost:8082", "http://localhost:8083")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}

这样配置后,只允许请求域名是 http://localhost:8082http://localhost:8082 并且请求服务端URL是 /signin/name 的请求才可以使用 CORS 机制。

3、CorsFilter

这种方法早在 SpringBoot1.x 版本中使用,使用方法如下:

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
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class MSCorsFilterConfig {
private CorsConfiguration getConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许任何域名使用
corsConfiguration.addAllowedOrigin("*");
// 允许任何头
corsConfiguration.addAllowedHeader("*");
// 允许任何HTTP方法
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setMaxAge(60L);
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source;
source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", getConfig());
return new CorsFilter(source);
}
}

这种方式跟上面的解决方法基本都是大同小异,大家根据实际情况选择使用,对提供的方法举一反三即可。

减少OPTIONS请求

虽然我们支持网页对服务端进行 OPTIONS 请求,但是请求如果多了势必会影响服务器性能。

如果在没有必要的情况下尽量减少由于跨域请求带来的 OPTIONS 请求,我们可以通过设置缓存时间来解决这个问题。

比如使用自定义 Filter 的方式设置600秒的缓存时间,示例代码如下:

1
response.setHeader("Access-Control-Max-Age", "600");

在大家进行测试的时候,记得不要勾选 Chrome 审查视图中 Network 选项中 Disable cache 这一项,否则每次都会进行 OPTIONS 请求,给你造成设置服务端缓存时间没有效果的假象。

常用Nginx命令

下面给出Windows版本的Nginx常用命令。

1、启动 Nginx

1
start nginx.exe

2、验证配置是否正确

1
nginx.exe -t

3、修改配置文件后,重新加载

1
nginx.exe -s reload

4、快速关闭

1
nginx.exe -s stop

5、正常关闭退出

1
nginx.exe -quit

6、查看Nginx版本

1
nginx.exe -V

坚持做好一件事,需要付出比常人更多的努力~

坚持原创技术分享!