《SpringBoot 3:入门与应用实战》第 9 章 跨域问题 阅读笔记 27

《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);
    }
}
相关推荐
山岚的运维笔记2 小时前
mysql 专业笔记 -- 第 8 章:使用变量
运维·数据库·笔记·后端·学习·mysql·dba
噜~噜~噜~2 小时前
操作系统笔记-2.3.6 管程
笔记·操作系统
aprilaaaaa5 小时前
(TC397 学习笔记)二、ICU和TIM的时钟计算
笔记·学习·fpga开发
hongmai6668886 小时前
ESP32-WROVER-IE-N4R8:外接天线+8MB内存,专治信号焦虑
笔记·单片机·嵌入式硬件·microsoft·risc-v
苏生Susheng6 小时前
【软件实施】Windows 服务器运维实战
java·运维·服务器·windows·spring boot·javaweb·实施
Persistent的粽子!6 小时前
C++:类与对象(一)
开发语言·c++·经验分享·笔记
xian_wwq6 小时前
【学习笔记】-深度认知系列-第8讲主流AI模型横向评测——GPT、Claude、Gemini、盘古、文心、通义
人工智能·笔记·学习
晨欣7 小时前
NVIDIA GPU 架构演进学习笔记(GPT-5.6 Terra 生成)
笔记·学习·gpu·显卡·nvidia·英伟达
一条破秋裤8 小时前
17_ADC原理与转换模式
笔记·stm32·学习