《SpringBoot 3:入门与应用实战》第 9 章 跨域问题 阅读笔记 27
10.4 跨域问题
10.4.1 同源策略与跨域问题
跨域是一种现象,跨域是由浏览器的同源策略引起的,而同源策略是在浏览器上对资源的一种安全保护策略,同源策略最初只是用于保护网页的Cookie,后来对同源的要求越来越严格。同源策略规定了 3 个"相同"条件:协议相同、域名相同、端口相同,当这 3 个条件都满足时,浏览器才会认为访问的资源是同一个来源。
同源策略的保护目标主要是 Cookie。试想如果访问一个银行的网站,当登录银行电子账户的同时又在浏览器中访问了别的网站,如果此时浏览器没有同源策略的保护,那么浏览器中存放的银行网站的 Cookie 会一并发送给别的网站。万一这个网站中有隐藏的攻击脚本,那么银行账户就会陷入危机。因此,同源策略主要保护的是上网安全。
简单地说,跨域就好比你在访问 https://a.com/ 域名下的网页,这个网页中 Ajax 请求 https://a.cn/ 的资源,此时就会构成跨域访问。因为当前的网页所在域名是 a.com,但网页内部访问的资源有不是当前域名的内容,所以就构成了跨域访问。只要触发以下 3 种情况之一,就会引起跨域问题。
- HTTP访问HTTPS,或者HTTPS访问HTTP。
- 不同域名之间的访问。
- 不同端口之间的访问。
目前,浏览器的同源策略在处理跨域问题时的态度有如下 3 种。
- 非同源的 Cookie、localstorage、indexedDB 无法访问。
- 非同源的 iframe 无法访问(防止加载其他网站的页面元素)。
- 非同源的 Ajax 请求可以访问,但浏览器拒绝接收响应。
10.4.2 演示跨域现象
要搭建能触发跨域问题的工程环境,可以借助 IDEA 构建两个工程运行实例,通过修改不同的 Web 容器端口就可以启动两个工程(模拟基于端口的跨域相对容易操作)。
1.IDEA 设置启动配置
依次单击 IDEA 的菜单栏的 "Run→Edit Configuration...",在弹出的对话框中选中当前的 SpringBootWebMvcApplication 主启动类后单击左上角的"复制"按钮,并在 "Build and run" 栏中添加运行参数 -Dserver.port=8081,之后单击 "保存" 就生成了一个在 8081 端口运行的 WebMvc 工程副本。

2.准备测试代码
接下来需要在页面中构造一个跨域的 Ajax 请求,在 userList.html 页面文件中找到 "批量删除" 按钮对应的事件绑定代码,将发送 Ajax 请求的地址改为发送到 8081 端口的实例上。

html
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户列表</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.js"></script>
</head>
<script>
function del(id) {
if (confirm("是否删除用户?")) {
window.location = document.getElementById("delete-link").getAttribute("href") + "?id=" + id;
}
}
$(function () {
$("#batch-delete-button").click(function() {
var selectedIds = $("[name='selectedId']:checked");
var ids = [];
for (var i = 0; i < selectedIds.length; i++) {
ids.push(selectedIds[i].value);
}
// $.post("[[@{/user/batchDelete}]]", {ids: ids}, function(data) {
// alert(data)
// });
$.post("http://localhost:8081/springboot-webmvc-a/user/batchDelete2", {ids: ids}, function(data) {
alert(data)
});
});
$("#batch-update-button").click(function() {
$("#batch-update-form").submit();
});
});
</script>
<body>
<h3>用户列表</h3>
<div>
<form id="query-form" method="get" th:action="@{/user/list}">
<label>用户名:</label>
<input type="text" name="username" value="">
<input type="submit" value="查询">
</form>
<button id="batch-delete-button">批量删除</button>
<button id="batch-update-button">批量修改用户名</button>
</div>
<table id="user-table" border="1">
<thead>
<tr>
<th width="40px"></th>
<th width="320px">id</th>
<th width="150px">用户名</th>
<th width="100px">姓名</th>
<th width="150px">生日</th>
<th width="150px">头像</th>
<th width="150px">所属部门</th>
<th width="100px">操作</th>
</tr>
</thead>
<tbody>
<form id="batch-update-form" th:action="@{/user/batchUpdate}" method="post">
<tr th:each="user,userstatus : ${userList}">
<td align="center">
<input type="checkbox" name="selectedId" th:value="${user.id}">
</td>
<td align="center">[[${user.id}]]</td>
<td align="center">
<input type="text" th:name="|users[${userstatus.index}].username|" th:value="${user.username}">
</td>
<td align="center">[[${user.name}]]</td>
<td align="center">[[${#dates.format(user.birthday, 'yyyy-MM-dd')}]]</td>
<td align="center">
<img th:src="@{|/user/getPhoto?id=${user.id}|}"/>
</td>
<td align="center">[[${user.department.name}]]</td>
<td align="center">
<a th:href="@{|/user/edit?id=${user.id}|}">编辑</a>
<a th:href="|javascript:del('${user.id}')|">删除</a>
</td>
</tr>
</form>
</tbody>
</table>
<a id="delete-link" th:href="@{/user/delete}" style="display: none"></a>
</body>
</html>
对应地要编写一个接口接收该请求。注意这次发送的请求 URI 前缀为 /springboot-webmvc-a/user/batchDelete2,需要再编写一个新的方法。

3.测试效果
代码准备完毕,接下来分别启动两个应用,在浏览器中访问 http://localhost:8080/springboot-webmvc-a/user/list,勾选几个用户后单击 "批量删除" 按钮,可以发现无论如何单击删除按钮,浏览器中都没有任何响应,打开 Chrome 的控制台后再次单击,在 Network 页签中看到当前请求被浏览器识别为跨域请求,在控制台中也有相应的 Access-Control-Allow-Origin 错误提示。与此同时还发现,后端 8081 实例中控制台有正确的输出内容,说明后端的确接收到该请求并予以处理,只是响应给浏览器后被浏览器拒绝,因此才出现了浏览器无响应的情况。


10.4.3 CORS 解决跨域问题
针对跨域问题,W3C 制定了一个标准,即跨域资源共享 (CORS)。CORS 的实现需要浏览器与服务端同时支持才可以,不过好在绝大多数主流浏览器支持,所以要实现的就是如何让服务端实现该标准,也就是如何在代码中配置实现。
WebMvc 提供的解决跨域问题的注解是 @CrossOrigin,它可以标注在需要的 Controller 类或 Handler 方法上,标注后即可以实现跨域资源共享。找到UserController,并在整个类上标注 @CrossOrigin 注解。之后重启工程实例,在浏览器上重新单击 "批量删除" 按钮,可以发现浏览器成功处理了该响应,并弹出提示框,证明 @CrossOrigin 注解解决跨域问题成功,此时观察该请求的响应头时可以发现WebMvc 附加了几个可以让浏览器处理的跨域允许信息。



10.4.4 @CrossOrigin 的细节
1.标注的位置
@CrossOrigin 可以标注在 Controller 类上,也可以标注在 Handler 方法上,这种写法比较类似于 @RequestMapping。标注在一个Controller 类上,则整个 Controller 中标注了 @RequestMapping 的方法都支持跨域访问;标注在某一个 Handler 方法上,则对应的方法支持跨域访问。
2.允许跨域的范围
跨域是可以指定请求来源的范围的,默认情况下 @CrossOrigin 的允许跨域范围是 *,也就是任意位置,如果需要限定允许跨域的范围,则可以自行声明。代码提供了一个简单的指定跨域范围的写法,用于指定跨域范围的属性是 origins,指定后由 http://localhost:8080 发送的请求才允许跨域访问。

3.@CrossOrigin 做的工作
究其根本,@CrossOrigin 只是一个便捷的解决跨域的方法而已,使用原生 Servlet 的 API 也可以完成相同的工作,因为解决跨域问题的核心是给响应头中添加一些额外的信息,其中最重要的就是上面刚提到的允许跨域的范围,也就是响应头中的 Access-Control-Allow-Origin: * 。所以 @CrossOrigin 完成的该工作其实就相当于用 HttpServletResponse 执行了一行代码:response.addHeader("Access- Control-Allow-Origin", "*");。

10.4.5 全局设置跨域
最后介绍一个全局设置跨域的方式,考虑到每个 Controller 中都需要标注 @CrossOrigin 注解会很麻烦,WebMvc 提供了一个可以全局设置跨域的扩展点,在 WebMvcConfigurer 中有一个 addCorsMappings 方法,该方法可以编程式注册跨域允许的映射信息,代码提供了一个简单示例,这段代码会将当前工程中所有的 GET 和 POST 请求都设置为允许跨域,但是实际开发中不建议读者这样做,最好是按需配置。
java
@Configuration
public class CrossOriginConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/*").allowedMethods("GET", "POST")
.allowedOrigins("*").allowCredentials(true);
}
}