在 Spring Boot 应用中设置 Cookie 的 SameSite 属性

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 属性。

参考资料:

通常,我们在 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 属性。

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