SameSite 是一个用于增强 Web 应用程序安全性的 Cookie 机制。它定义了浏览器在发送跨站请求时是否应该附加 Cookie。旨在防止跨站请求伪造(CSRF)攻击和某些类型的跨站信息泄露攻击。
SameSite 属性是可选的,它有三个可选值:
-
Strict
(严格模式):在严格模式下,浏览器只会在用户访问与 Cookie 关联的站点时发送 Cookie。如果请求来自其他站点(包括跨站请求),浏览器将不会发送 Cookie。这样可以有效地防止跨站请求伪造攻击。 -
Lax
(宽松模式):在宽松模式下,大多数情况下,浏览器只会在用户访问与 Cookie 关联的站点时发送 Cookie。但是,如果用户从外部站点通过 GET 方法访问当前站点的 URL,浏览器会发送 Cookie。这样可以在某些常见的使用情况下保持用户体验,同时仍然提供一定程度的安全性。总结如下:| 请求类型 | 示例 | 正常情况 | Lax | |---------|----------------------------------|-----------|-----------| | 链接 |
<a href=...></a>
| 发送 Cookie | 发送 Cookie | | 预加载 |<link rel=prerender href=.../>
| 发送 Cookie | 发送 Cookie | | GET 表单 |<form method=GET action=...>
| 发送 Cookie | 发送 Cookie | | POST 表单 |<form method=POST action=...>
| 发送 Cookie | 不发送 | | iframe |<iframe src=...></iframe>
| 发送 Cookie | 不发送 | | AJAX |$.get(...)
| 发送 Cookie | 不发送 | | Image |<img src=...>
| 发送 Cookie | 不发送 | -
None
(无限制模式):在无限制模式下,浏览器会在跨站请求时始终发送 Cookie。这意味着即使请求来自其他站点,浏览器也会发送 Cookie。这种模式需要慎重使用,因为它可能导致安全风险,可能会被滥用。必须同时设置 Cookie 的
Secure
属性(表示 Cookie 只会在 HTTPS 协议中传输),如:SameSite=None; Secure
,否则无效。
本文将会带你了解如何在 Spring Boot 应用中设置 Cookie 的 SameSite 属性。
参考资料:
Servlet Cookie 还未实现 {#servlet-cookie-还未实现}
通常,我们在 Spring Boot 应用中通过 Servlet 的 Cookie
对象来设置 Cookie 到浏览器。如下:
@GetMapping
public void setCookie (HttpServletResponse response) {
// 创建 Cookie
Cookie cookie = new Cookie("Hello", "Spring 中文网");
cookie.setMaxAge(-1); // 浏览器关闭,则删除 Cookie
cookie.setSecure(true); // 仅在 HTTPS 协议中传输
cookie.setHttpOnly(true); // Javascript 不能读写
// cookie.setDomain(null); // 提交 cookie 的域
// cookie.setPath(null); // 提交 cookie 的 path
//添加 cookie 到客户端
response.addCookie(cookie);
}
但是,直到撰稿时最新版的 JakartaEE 6 Servlet Api 中的 Cookie
类,仍然未实现这个规范。所以目前,通过这种方式,我们无法设置 Cookie 的 SameSite
属性。
使用 ResponseCookie 工具类设置 Cookie {#使用-responsecookie-工具类设置-cookie}
Cookie 本质上也是通过 Set-Cookie
响应头进行设置的,因此我们可以自定义 Set-Cookie
响应头来设置 Cookie,从而可以实现设置 SameSite 属性。
对于这么普遍的需求,Spring 早已想到。它提供了一个 ResponseCookie
工具类,可以让我们通过设置 Header 的方式来设置 Cookie。
使用方式很简单,我们创建一个演示 Controller 来进行测试。如下:
package cn.springdoc.demo.controller;
import org.springframework.boot.web.server.Cookie.SameSite;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/test/cookie")
public class DemoController {
@GetMapping
public void setCookie (HttpServletResponse response) {
// key & value
ResponseCookie cookie = ResponseCookie.from("Hello", "World")
.maxAge(-1) // 浏览器关闭,则删除 Cookie
.secure(false) // 可以在 HTTP 协议中传输
.httpOnly(true) // Javascript 不能读写
// .domain(null) // 提交 cookie 的域
// .path(null) // 提交 cookie 的path
.sameSite(SameSite.LAX.attributeValue()) // 设置 SameSite 为 LAX
.build()
;
// 设置Cookie
response.setHeader(HttpHeaders.SET_COOKIE, cookie.toString());
}
}
SameSite
枚举是 org.springframework.boot.web.server.Cookie
的内部类,它定义了 SameSite 的枚举值,如下:
public enum SameSite {
NONE("None"),
LAX("Lax"),
STRICT("Strict");
private final String attributeValue;
SameSite(String attributeValue) {
this.attributeValue = attributeValue;
}
public String attributeValue() {
return this.attributeValue;
}
}
测试 {#测试}
启动应用,打开浏览器,打开控制台,访问 http://localhost:8080/test/cookie
端点。
观察网络面板中该请求的响应原文,如下:
HTTP/1.1 200
Set-Cookie: Hello=World; HttpOnly; SameSite=Lax
Content-Length: 0
Date: Wed, 13 Sep 2023 09:57:54 GMT
Keep-Alive: timeout=60
Connection: keep-alive
如你所见,其中 Set-Cookie: Hello=World; HttpOnly; SameSite=Lax
表示已经成功设置了 Cookie,且 SameSite
属性为 Lax
。