从零开始学习CSRF:浏览器的工作原理、相关攻击方式以及Spring Security中的实现方法【完整手册】
如果你曾经构建过Web应用程序或配置过Spring Security,那么你几乎肯定遇到过跨站请求伪造攻击(CSRF)。 OAuth 2.0的工作原理:后端开发人员的实用指南 》中,我简要介绍过那个神秘的 state 参数,并指出它的核心作用就是保护授权流程免受CSRF攻击的侵害。 当时,我们只是把CSRF视为一个需要了解的基本概念而已。而今天,我们将深入探讨这一主题。 也许你在使用Spring Boot构建REST API时,会遇到每次发送 POST 请求时都会出现的HTTP 403 Forbidden错误,于是你通过在Security Filter Chain中添加 .csrf(csrf
如果你曾经构建过Web应用程序或配置过Spring Security,那么你几乎肯定遇到过跨站请求伪造攻击(CSRF)。
OAuth 2.0的工作原理:后端开发人员的实用指南》中,我简要介绍过那个神秘的state参数,并指出它的核心作用就是保护授权流程免受CSRF攻击的侵害。
当时,我们只是把CSRF视为一个需要了解的基本概念而已。而今天,我们将深入探讨这一主题。
也许你在使用Spring Boot构建REST API时,会遇到每次发送POST请求时都会出现的HTTP 403 Forbidden错误,于是你通过在Security Filter Chain中添加.csrf(csrf -> csrf.disable())代码来“解决”这个问题。
大多数教程都将CSRF视为一个需要配置的选项,或者是一个框架级别的设置。它们会直接跳到代码示例部分:
// 大多数教程在第一行就会这样写:
http.csrf(Customizer.withDefaults());
从框架配置入手来讲解Web安全机制,其实掩盖了Web安全背后的真正原理。Spring Security并不是凭空创造这些安全规则的,它是根据Web浏览器、HTTP协议以及Cookie的工作原理来制定相应策略的。
在这本手册中,我们将采取自下而上、基于基本原理的方式来讲解Web安全相关内容。在彻底了解浏览器、HTTP头部信息、会话管理机制以及跨站请求伪造攻击的运作原理之前,我们不会讨论Spring Security的相关内容。
读完这本指南后,你将会明白:
为什么浏览器会在发出的请求中自动附加认证信息。
为什么这种自动行为会带来严重的安全漏洞。
为什么攻击者根本不需要窃取或读取你的Cookie就能利用CSRF攻击机制。
为什么同源策略(SOP)和CORS并不能有效防止CSRF攻击。
现代的安全防护措施,比如CSRF令牌和
SameSiteCookie,其内部工作原理是什么。Spring Security是如何在内部实现这些安全机制的,以及如何有效地配置它们。
让我们先抛开框架的束缚,来看看Web究竟是如何工作的。
目录
CSRF出现之前的问题
要理解安全机制,我们首先必须了解“状态”这一概念。
超文本传输协议(HTTP)本质上是无状态的。这意味着,如果Alice在上午10点向travelbuddy.com发送了一个HTTP请求,然后在上午10点01分再次发送另一个HTTP请求,服务器会将这两个请求视为完全独立、毫无关联的事件来处理。
如果没有机制来在多次请求之间保持对用户身份的识别,Alice就必须在每一次HTTP请求中都输入自己的用户名和密码。这对用户体验和系统性能来说都是极其不利的。
在会话机制成为标准之前,开发者们尝试通过查询参数或在每次点击时添加基本认证头部来传递用户凭证。这样一来,这些凭证就会被记录在服务器日志、浏览器历史记录以及共享的URL中,从而导致安全风险。
会话和Cookie是如何解决这个问题的?
为了解决这个问题,网页工程师引入了服务器端会话和HTTP Cookie这一概念。
当Alice通过POST请求将用户名和密码发送到https://travelbuddy.com/login进行登录时,服务器会验证她的凭证。服务器不会在下一个页面要求她再次登录,而是会在内存中(或数据库/Redis缓存中)创建一个会话,并为这个会话分配一个唯一且不可预测的标识符:即会话ID。
随后,服务器会使用一个特殊的HTTP响应头部Set-Cookie将这个会话ID发送回Alice的浏览器。
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: JSESSIONID=abc123xyz789; Path=/; Secure; HttpOnly
当Alice的浏览器收到这个响应时,它会读取Set-Cookie头部信息,并提取出JSESSIONID=abc123xyz789,将其存储在浏览器的内部存储空间中,也就是Cookie缓存区里。
现在,Alice就已经“登录”成功了。服务器通过这个会话记录来识别她的身份,而浏览器则保存着访问该会话所需的密钥——即JSESSIONID。
为什么浏览器会自动发送Cookie
现在我们来到了网页技术早期做出的一个关键设计决策。
一旦浏览器为travelbuddy.com这个域名在Cookie缓存区中存储了JSESSIONID=abc123xyz789,那么在后续的请求中,这个Cookie又是如何被发送回服务器的呢?
开发者是否必须编写自定义的JavaScript代码来添加这些cookie?不需要。
浏览器被专门设计用来自动处理cookie的管理工作。
请求生命周期与cookie的自动添加
每当爱丽丝的浏览器准备向https://travelbuddy.com发送HTTP请求时(无论是因为爱丽丝点击了某个链接、提交了HTML表单,还是JavaScript触发了fetch()方法),浏览器都会按照以下步骤进行操作:
检查目标URL:浏览器会查看目标地址(例如
https://travelbuddy.com/api/connections)。查找存储的cookie:浏览器会检查其cookie存储区中,那些域名和路径与
travelbuddy.com匹配的cookie。验证cookie的有效性:浏览器会确认这些cookie是否已经过期,以及诸如
Secure这样的安全设置是否得到了遵守。将cookie添加到请求头中:如果找到了有效的cookie,浏览器会自动将这些cookie添加到发出的HTTP请求中。
下面是爱丽丝的浏览器发出的请求的实际内容:
POST /api/connections/add HTTP/1.1
Host: travelbuddy.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Accept: text/html,application/xhtml+xml
Cookie: JSESSIONID=abc123xyz789
Content-Type: application/x-www-form-urlencoded
service=SkyScanner
请注意一个关键点:无论是爱丽丝,还是任何自定义的JavaScript代码,都没有显式地添加Cookie: JSESSIONID=abc123xyz789这个cookie。
浏览器在将请求数据发送到网络之前,会自动将其添加到请求头中。对于服务器来说,接收到这个cookie就意味着该请求是来自爱丽丝的已登录会话。
这种自动处理机制非常方便,它使得用户在重新加载页面或点击链接时能够无缝地继续浏览网页。但正如我们接下来将会看到的,这种便利性也会带来安全隐患。
当自动添加cookie变得危险时
那么,自动添加cookie这一行为本身是否构成一种安全漏洞呢?
并不构成。如果爱丽丝只访问travelbuddy.com这个网站,那么这种自动添加cookie的功能是完全正常的。
问题出现在这样一个实际情况中:爱丽丝在同一个浏览器会话中访问了多个网站。
以evil.com为例
假设爱丽丝在标签页1中登录到了TravelBuddy网站。她的会话cookie(JSESSIONID=abc123xyz789)被安全地保存在浏览器中,专门用于travelbuddy.com这个网站。
在第二个标签页中,Alice访问了一个与她无关的网站:https://evil.com(也许她是点击了一封钓鱼邮件或论坛帖子中的链接才进入这个网站的)。
evil.com实际上是由攻击者控制的。攻击者知道TravelBuddy有一个用于连接第三方服务的接口,其地址为https://travelbuddy.com/api/connections/add。攻击者想诱使Alice将这个恶意服务连接到她的账户上。
攻击者在evil.com提供的网页中嵌入了以下隐藏的HTML表单:
<!-- 该页面托管在 https://evil.com/win-a-car.html 上 -->
<!DOCTYPE html>
<html>>
<body>
<h1>您赢了一次免费旅行!请点击下方领取。
<<>script>
// 页面一加载就自动提交表单
document.getElementById('maliciousForm').submit();
</body>>
</html>>
攻击过程的详细步骤
让我们一步步来看当Alice打开https://evil.com/win-a-car.html时会发生什么:
Alice的浏览器从
evil.com获取并解析HTML内容。浏览器遇到
标签后,会执行document.getElementById('maliciousForm').submit()这段代码。浏览器准备发送一个指向
https://travelbuddy.com/api/connections/add的POST请求。浏览器会检查目标服务器的地址:
travelbuddy.com。浏览器会查看自己的Cookie存储框:"我是否有针对
travelbuddy.com的有效Cookie?"有!浏览器找到了
JSESSIONID=abc123xyz789(这是Alice在第一个标签页中使用的会话Cookie)。浏览器会将
Cookie: JSESSIONID=abc123xyz789自动添加到发送到travelbuddy.com的请求数据中。这个请求最终会到达
TravelBuddy的后端Spring Boot服务器。
从服务器的角度来看
下面是TravelBuddy后端在处理这个请求时所看到的内容:
POST /api/connections/add HTTP/1.1
Host: travelbuddy.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Content-Type: application/x-www-form-urlencoded
Cookie: JSESSIONID=abc123xyz789
service=MaliciousAttackerService
TravelBuddy服务器会检查Cookie头信息,并将其与自己的会话存储数据进行比对。验证结果显示,JSESSIONID=abc123xyz789确实属于Alice的会话!
服务器认为:"Alice发送了一个POST请求,用于添加MaliciousAttackerService。她已经通过了身份验证,因此我会批准这个请求。"
服务器更新了Alice的账户状态。MaliciousAttackerService现在已与她的个人资料关联起来。
CSRF的核心实现原理
让我们退一步,仔细分析刚才发生了什么:
攻击者根本没有看到或窃取Alice的会话cookie。由于浏览器的隔离机制,《evil.com》上的攻击者无法读取属于《travelbuddy.com》的cookie。
攻击者也没有破解任何加密措施。整个过程中都使用了HTTPS协议。
攻击者只是诱使Alice的浏览器发出了请求。浏览器严格遵循其自动添加cookie的规则,从而替攻击者传递了认证信息。可以说,攻击者是在“偷窃Alice的cookie时被当场抓住了”!
这就是跨站请求伪造攻击的实际过程:攻击者诱使受害者的浏览器向一个受害者已经通过身份验证的受信任网站发送不必要的、会改变系统状态的HTTP请求。
直观理解这一攻击过程
通过可视化Alice、浏览器、《evil.com》以及《TravelBuddy》之间的交互过程,我们可以清楚地了解整个请求流程。
1. 完整的CSRF攻击序列
这次攻击分为三个阶段,涉及四个主要参与者:Alice、她的浏览器、《TravelBuddy》的后端服务器,以及运行在《evil.com》上的攻击者网站。
在第一阶段,Alice在《TravelBuddy》网站上进行了身份验证。她通过浏览器提交了登录信息,浏览器随后向《TravelBuddy》后端发送了POST请求。后端验证了她的身份信息,并返回了一个HTTP 200 OK状态码,同时附带一个包含JSESSIONID=abc123xyz的Set-Cookie头部信息。
收到这个响应后,Alice的浏览器会自动将这个会话标识符保存到其cookie中,且该cookie是针对travelbuddy.com域名设置的。
在第二阶段,攻击者设置了陷阱。在保持《TravelBuddy》标签页处于活动状态的同时,Alice打开了另一个浏览器标签页,访问了《evil.com》。她的浏览器向《evil.com》请求了win-a-car.html页面。作为响应,《evil.com》返回了一份HTML文档,其中包含一个针对《TravelBuddy》系统的隐藏表单,以及一段会在页面加载后立即执行的JavaScript脚本。
在最终阶段,攻击会自动执行。evil.com上的恶意JavaScript代码会调用form.submit(),命令浏览器向https://travelbuddy.com/api/connections/add发送一个POST请求。
在将请求发送到网络之前,浏览器会检查自己的cookie存储区中是否存在与travelbuddy.com相关的cookie。它会找到Alice的活跃会话cookie,并自动将Cookie: JSESSIONID=abc123xyz添加到要发送的请求数据中。TravelBuddy服务器收到请求后,会验证这个有效的会话cookie,认为Alice确实是想要执行这个操作,于是就会将攻击者的服务关联到她的账户上。
2. 浏览器在发送请求时的决策过程
每当有请求被发起时,浏览器都会根据一定的规则来决定是否需要添加cookie到请求数据中:
这张图展示了每当有任何标签页或脚本触发HTTP请求时,浏览器会自动执行的验证流程。
这个过程会在任何HTTP请求被发起的瞬间开始。浏览器首先会检查目标URL,以确定其所属的域名,比如travelbuddy.com。一旦确定了域名,浏览器就会查询自身的cookie存储区,看是否存在与该域名相关的cookie。如果找不到匹配的cookie,浏览器就会直接发送原始的HTTP请求,而不会添加任何认证信息。
如果找到了匹配的cookie,浏览器会验证这些cookie的有效性。它会检查这些cookie是否已经过期,请求路径是否与cookie中定义的路径一致,以及诸如Secure这样的安全设置是否得到满足。如果有任何验证失败,这些cookie就会被丢弃,请求也会在不包含认证信息的情况下继续执行。但如果cookie是有效且处于活跃状态的,浏览器就会构建一个Cookie头字段,其中包含存储的会话密钥,然后将其添加到要发送的HTTP请求数据中,再通过网络将请求发送给服务器。
3. 会话与cookie的生命周期状态图

这个状态图展示了用户在一次网页会话中如何在安全状态、已认证状态和易受攻击的状态之间切换。
当用户首次打开网络浏览器时,他们处于未认证状态,此时目标应用程序没有任何存储的cookie。通过登录表单提交有效的凭证后,用户就会进入已认证状态。在这种状态下,服务器会发送一个Set-Cookie头部信息,使得浏览器将其会话ID保存在cookie中。此后,每当用户向该应用程序发出请求时,浏览器都会自动附加这个cookie,从而保持用户的登录状态。
当一个已认证的用户打开第二个标签页并访问一个不受信任的网站,而他们的应用程序会话仍然处于活动状态时,就会出现一种安全漏洞。这种操作会使浏览器的环境进入容易受到跨站请求伪造攻击的状态。如果这个不受信任的网站向原始应用程序发送跨站请求,浏览器自动附加cookie的机制就会被触发,从而导致服务器上发生未经授权的状态变更。只有当用户登出或服务器会话过期时,这一循环才会结束,客户端才会恢复到最初的未认证状态。
为什么浏览器没有出现故障
当开发者首次了解跨站请求伪造这个概念时,他们的第一反应往往是:“这简直是浏览器的严重缺陷!为什么浏览器厂商不干脆完全禁用自动发送cookie的功能呢?”
要理解为什么浏览器会表现出这种行为,我们就必须考虑网页兼容性以及安全工程领域中的一个概念——环境权限。
环境权限原理
当一个系统在用户没有明确表示要进行某项具体操作的情况下,自动将其身份信息应用到用户的所有操作中时,这个系统就是在使用环境权限这一机制。
HTTP cookie就属于这种环境凭证。如果你已经登录了,那么任何带有目标URL的请求都会自动包含你的认证信息。
为什么浏览器厂商不会简单地“修复”这个问题
万维网的本质就是一个由相互关联的超媒体文档构成的网络。跨站交互是网页设计的基本特性,而不是偶然出现的错误:
图片和资源文件:当
news.com嵌入了托管在cdn.com上的图片时,你的浏览器会向cdn.com发送跨站请求。跨站表单提交:在早期网页环境中(直到今天),使用PayPal进行支付意味着用户需要在
e-commerce.com上填写HTML表单,然后这些数据会直接发送到paypal.com。超链接:点击
google.com上的链接时,浏览器会通过跨站GET请求将用户带到wikipedia.org。
如果浏览器突然默认不再在跨站请求中附加cookie,那么过去三十年来建立的数百万个旧网站将会立即出现故障。用户一旦点击来自电子邮件、搜索引擎或社交媒体的链接,就会被强制登出。
浏览器制造商更重视向后兼容性。他们并没有取消跨站功能,而是提供了可配置的安全设置,开发者可以根据需要选择是否启用这些设置。
要理解这些安全设置,我们首先需要了解最基础的浏览器安全模型:同源策略。
同源策略
许多开发者会认为:“同源策略不就是用来阻止跨站请求的吗?”
这是网页开发中最为常见的误解之一。让我们来澄清一下,同源策略究竟是什么,以及它的作用是什么。
定义“起源”概念
在网络安全领域,“起源”由三个组成部分来确定:
协议(例如:
http与https的区别)域名(例如:
travelbuddy.com)端口(例如:
:80、:443或:8080)
只有当这三个组成部分完全相同时,两个URL才被视为具有相同的起源。
| URL 1 | URL 2 | 是否同源? | 原因 |
|---|---|---|---|
https://travelbuddy.com/page1 |
https://travelbuddy.com/page2 |
是 | 协议、域名和端口都相同。 |
http://travelbuddy.com/page1 |
https://travelbuddy.com/page1 |
否 | 协议不同(http与https有区别)。 |
https://travelbuddy.com/page1 |
https://api.travelbuddy.com/page1 |
否 | 域名不同(travelbuddy.com与api.travelbuddy.com有区别)。 |
https://travelbuddy.com:8080 |
https://travelbuddy.com:9090 |
否 | 端口不同(8080与9090有区别)。 |
同源策略保护什么,允许什么
同源策略规定了在同一起源域中运行的脚本如何与另一起源域中的资源进行交互。
同源策略的基本原则是:该策略禁止脚本读取其他起源域的响应数据;但通常情况下,它并不会阻止脚本或HTML代码向其他起源域发送请求。
我们需要重点强调这一区别:
发送请求时:evil.com可以创建如下这样的HTML表单:。当表单被提交后,浏览器会将请求发送到travelbuddy.com。后端会处理这个请求,并修改数据库中的数据。
读取响应时:运行在evil.com上的JavaScript会试图查看travelbuddy.com返回的HTTP响应内容。但由于evil.com和travelbuddy.com属于不同的域名,浏览器会阻止JavaScript读取这些数据。
需要注意,CSRF攻击与数据读取无关:CSRF攻击针对的是数据库状态的变化,而非数据的检索。
攻击者根本不需要查看travelbuddy.com返回的响应内容,他们的目的仅仅是触发服务器上的相应操作。由于同源策略只阻止响应内容的读取,而不影响请求的执行,因此仅依靠同源策略是无法有效防止CSRF攻击的。
为什么CORS无法预防CSRF攻击
这就引出了另一个容易引起混淆的概念:跨源资源共享(CORS)。
在开发者论坛中,每当有人遇到CSRF相关问题或跨站请求问题时,人们常常会建议:“只需在后端正确配置CORS即可!”
我们需要明确一点:CORS并不能预防CSRF攻击。事实上,CORS的作用是放宽同源策略的限制,而不是增加新的安全措施。
再次探讨读取与发送操作的区别
请记住:默认情况下,同源策略会阻止跨源数据读取操作。
CORS是一种机制,它允许服务器明确告诉浏览器:“我信任运行在trusted-partner.com上的JavaScript,你可以允许trusted-partner.com读取我的响应内容。”
CORS实际上是一种可选的机制,用于允许跨源数据读取。如果禁用或错误配置CORS,也无法阻止浏览器发送伪造的请求。
简单请求与预检请求的区别
要理解为什么CORS无法有效防止CSRF攻击,我们就需要了解浏览器在CORS规则下是如何处理跨源HTTP请求的。浏览器将跨源请求分为两类:
简单请求
预检请求
1. 简单请求
如果一个请求满足以下所有条件,那么它就被视为简单请求:
使用HTTP方法:
GET、HEAD或POST。使用标准的浏览器内容类型:
application/x-www-form-urlencoded、multipart/form-data或text/plain。不设置自定义的HTTP头部信息(如
X-Requested-With或Authorization)。
当浏览器遇到一个简单请求时(例如通过标准HTML表单发送的POST请求),它会立即将这个请求发送到目标服务器。
如图所示,服务器会在接收到请求的瞬间立即执行SQL的UPDATE或INSERT操作。而当浏览器开始检查返回响应中的CORS头部信息时,服务器上的状态变化已经发生了。
2. 预检请求
如果一个请求使用了非标准的方法(如PUT、DELETE),或非标准的内容类型(如application/json),又或者设置了自定义的头部信息,那么浏览器会首先发送一个名为OPTIONS的请求,这个请求被称为预检请求。
由于OPTIONS预检请求不会产生任何副作用,并且会在实际请求被发送之前进行检查,因此CORS机制会间接地阻止来自未经授权域的跨源JSON请求。
但是,仅仅依赖CORS来保障安全性是危险的:攻击者完全可以使用标准的HTML表单提交方式,重新切换为简单请求(如application/x-www-form-urlencoded),从而完全绕过CORS的预检机制。
安全方法与状态变化
在探讨有效的防御措施之前,我们首先需要了解HTTP规范中定义的一个架构概念:安全方法与幂等性。
HTTP方法是根据它们对服务器状态的预期影响来分类的:
安全请求方法:
GET、HEAD、OPTIONS和TRACE这些方法被定义为只读操作。它们绝对不能改变服务器的状态(例如,获取用户信息或读取航班列表)。不安全/会修改状态的方法:
POST、PUT、DELETE和PATCH这些方法用于执行特定操作、修改数据库数据、创建新资源或触发事务处理。
开发人员的不当行为:会修改状态的GET请求
假设TravelBuddy团队中的一名初级开发者编写了如下代码:
// ❌ 危险代码:通过GET请求修改状态
GetMapping("/api/connections/delete")
public String deleteConnection(@RequestParam String serviceId, HttpSession session) {
User user = (User) session.getAttribute("user");
connectionService.deleteForUser(user, serviceId);
return "redirect:/dashboard";
}
为什么这会构成架构上的错误,并带来严重的安全风险呢?
因为evil.com上的攻击者根本不需要使用HTML表单或JavaScript就能发起GET请求。他们只需使用简单的HTML标签就可以实现这一目的:
<!-- 位于evil.com上 -->
<img src="https://travelbuddy.com/api/connections/delete?serviceId=SkyScanner" width="0" height="0" />
当Alice的浏览器解析来自evil.com的HTML代码时,会遇到标签。为了显示页面内容,浏览器会自动向https://travelbuddy.com/api/connections/delete?serviceId=SkyScanner发送GET请求,并自动附加Alice的session cookie。
后端接收到这个请求后,会执行connectionService.deleteForUser(...)方法,从而导致Alice的相关信息被删除!
网络安全的第一条规则
GET请求必须始终是安全且只读的。在任何GET处理程序中,都不得执行任何会修改状态的操作(如创建、更新或删除数据)。
确保GET请求的安全性是网络安全的基础。但仅仅让GET请求保持只读状态,其实只能有效防范通过图像标签进行的攻击;对于POST、PUT或DELETE请求而言,这种防护措施仍然无法有效防止CSRF攻击。
对于那些会修改系统状态的请求,我们需要采取专门的防御措施。
CSRF令牌(同步令牌机制)
现在你已经了解了这一核心安全漏洞的原理(即浏览器会在跨站请求中自动附加用户的cookie信息),那么你就可以在应用程序中加入标准的安全防护措施了。
在CSRF令牌出现之前,存在什么问题?
服务器无法区分以下两种请求:一种是用户通过travelbuddy.com的正规用户界面主动发出的HTTP请求,另一种则是由evil.com伪造的请求——这种伪造请求会导致浏览器自动附加用户的cookie。
从服务器的角度来看,这两种请求看起来完全一样:相同的会话cookie、目标URL以及数据格式。
CSRF令牌是如何解决这个问题的?
为了区分真实请求和伪造请求,我们必须要求有一种只有真正应用程序才知道的证据,而外部攻击者根本无法伪造或获取这种证据。
这种防御机制被称为同步令牌机制(或CSRF令牌)。
同步令牌机制的工作原理
令牌生成:当Alice登录或请求包含表单的页面时,服务器会生成一个加密强度高、随机且不可预测的字符串(例如128位的SecureRandom UUID)。
会话存储:服务器会将这个生成的字符串与Alice的服务器端会话状态关联起来。
令牌插入用户界面:服务器会在发送给Alice的HTML响应中包含这个令牌,通常是以表单中的隐藏输入字段形式存在,或者作为JavaScript可以读取的meta标签。
令牌提交:当Alice提交表单时,她的浏览器会将这个隐藏的令牌通过请求体发送回去(或者作为自定义的HTTP头部信息)。
服务器验证:服务器会将收到的令牌与Alice服务器端会话中保存的令牌进行比对。
如果两个令牌匹配:请求是真实的,则继续处理该请求。
如果两个令牌不匹配(或者令牌缺失):请求是伪造的,应使用HTTP 403 Forbidden响应拒绝该请求。
HTML表单示例
以下是TravelBuddy如何呈现受保护的表单的例子:
<!-- 由TravelBuddy在https://travelbuddy.com/connect-service生成 -->
<form action="/api/connections/add" method="POST">
<!-- 标准表单字段 -->
<label for="service">>服务名称:
2026 Copyright © 上海知力信息科技有限公司 · All Rights Reserved
沪ICP备09012674号
沪公网安备 31011502018658号 增值电信业务许可证:沪B2-20220251