知識庫 / Spring / Spring Security RSS 訂閱

Spring Security 中 CSRF 保護指南

Spring Security
HongKong
5
02:51 PM · Dec 06 ,2025

1. 概述

在本文中,我們將討論跨站請求偽造 (CSRF) 攻擊以及如何使用 Spring Security 來防止它們。

2. 簡單的 CSRF 攻擊

存在多種 CSRF 攻擊形式。我們來討論一些最常見的攻擊。

2.1. GET 示例

以下 GET 請求由已登錄的用户用於向特定銀行賬户 1234 轉移資金:

GET http://bank.com/transfer?accountNo=1234&amount=100

如果攻擊者想要從受害者的賬户轉賬到自己的賬户時,則需要讓受害者觸發該請求:<em>5678 —

GET http://bank.com/transfer?accountNo=5678&amount=1000

可以有多種方法來實現這一點:

  • 鏈接 – 攻擊者可以誘使受害者點擊此鏈接,例如,以執行轉賬:
<a href="http://bank.com/transfer?accountNo=5678&amount=1000">
Show Kittens Pictures
</a>
  • 圖片 – 攻擊者可能會使用帶有目標URL作為圖片來源的<img/>標籤。換句話説,點擊並不需要。請求會在頁面加載時自動執行:
<img src="http://bank.com/transfer?accountNo=5678&amount=1000"/>

2.2. POST 示例

假設主請求需要是一個 POST 請求:

POST http://bank.com/transfer
accountNo=1234&amount=100

在這種情況下,攻擊者需要讓受害者執行一個類似的請求:

POST http://bank.com/transfer
accountNo=5678&amount=1000
<p>Neither the <em>&lt;a&gt;</em> nor the <em>&lt;img/&gt;</em> tags will work in this case.</p>
<p>The attacker will need a <em>&lt;form&gt;</em>:</p>
<form action="http://bank.com/transfer" method="POST">
    <input type="hidden" name="accountNo" value="5678"/>
    <input type="hidden" name="amount" value="1000"/>
    <input type="submit" value="Show Kittens Pictures"/>
</form>

然而,可以使用 JavaScript 自動提交表單。

<body onload="document.forms[0].submit()">
<form>
...

2.3. 實際模擬

現在我們已經瞭解了 CSRF 攻擊的運作方式,接下來讓我們在 Spring 應用中模擬這些示例。

我們將從一個簡單的控制器實現——BankController 開始:

@Controller
public class BankController {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @RequestMapping(value = "/transfer", method = RequestMethod.GET)
    @ResponseBody
    public String transfer(@RequestParam("accountNo") int accountNo, 
      @RequestParam("amount") final int amount) {
        logger.info("Transfer to {}", accountNo);
        ...
    }

    @RequestMapping(value = "/transfer", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    public void transfer2(@RequestParam("accountNo") int accountNo, 
      @RequestParam("amount") final int amount) {
        logger.info("Transfer to {}", accountNo);
        ...
    }
}

讓我們也創建一個基本的 HTML 頁面,來觸發銀行轉賬操作:

<html>
<body>
    <h1>CSRF test on Origin</h1>
    <a href="transfer?accountNo=1234&amount=100">Transfer Money to John</a>
	
    <form action="transfer" method="POST">
        <label>Account Number</label> 
        <input name="accountNo" type="number"/>

        <label>Amount</label>         
        <input name="amount" type="number"/>

        <input type="submit">
    </form>
</body>
</html>

這是主應用程序的頁面,運行在源域名上。

請注意,我們通過一個簡單的鏈接實現了 GET 請求,並通過一個簡單的 <form> 實現了 POST 請求。

現在讓我們看看攻擊者頁面的樣子:

<html>
<body>
    <a href="http://localhost:8080/transfer?accountNo=5678&amount=1000">Show Kittens Pictures</a>
    
    <img src="http://localhost:8080/transfer?accountNo=5678&amount=1000"/>
	
    <form action="http://localhost:8080/transfer" method="POST">
        <input name="accountNo" type="hidden" value="5678"/>
        <input name="amount" type="hidden" value="1000"/>
        <input type="submit" value="Show Kittens Picture">
    </form>
</body>
</html>

此頁面將運行在不同的域—攻擊者域。

最後,讓我們在本地同時運行原始應用程序和攻擊者應用程序。

為了使攻擊成功,用户需要使用帶有會話 cookie 的會話 cookie 身份驗證到原始應用程序。

讓我們首先訪問原始應用程序頁面:

http://localhost:8081/spring-rest-full/csrfHome.html

它將設置我們的瀏覽器上的 JSESSIONID cookie。

接下來,我們訪問攻擊者頁面:

http://localhost:8081/spring-security-rest/api/csrfAttacker.html

如果我們跟蹤從該攻擊者頁面發出的請求,就能識別出哪些請求指向了原始應用程序。由於 JSESSIONID cookie 會自動包含在這些請求中,Spring 會將它們識別為來自原始域名的請求。

3. Spring MVC 應用

為了保護 MVC 應用,Spring 會為每個生成的視圖添加 CSRF 令牌。該令牌必須在每個修改狀態的 HTTP 請求中提交(包括 PATCH、POST、PUT 和 DELETE — 並非 GET)。 這樣做可以保護我們的應用程序免受 CSRF 攻擊,因為攻擊者無法從他們的頁面獲取該令牌。

接下來,我們將看到如何配置應用程序安全性和如何使客户端與之兼容。

3.1. Spring Security 配置

在較早的XML配置(Spring Security 4 之前)中,CSRF(跨站請求偽造)保護默認情況下已禁用,並且我們可以根據需要啓用它:

<http>
    ...
    <csrf />
</http>

從 Spring Security 4.x 版本開始,CSRF 保護默認已啓用。

此默認配置會將 CSRF 令牌添加到 HttpServletRequest 屬性 _csrf 中。

如果需要,我們可以禁用此配置:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf(AbstractHttpConfigurer::disable);
    return http.build();
}

3.2. 客户端配置

現在我們需要在請求中包含 CSRF 令牌。

<em>_csrf</em> 屬性包含以下信息:

  • token – CSRF 令牌值
  • parameterName – HTML 表單參數名稱,必須包含令牌值
  • headerName – HTTP 頭部名稱,必須包含令牌值

如果我們的視圖使用 HTML 表單,我們將使用 <em>parameterName</em> 和 <em>token</em> 值來添加隱藏輸入:

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

如果我們的視圖使用 JSON,則需要使用 headerNametoken 值來添加 HTTP 標頭。

我們首先需要將 token 值和標頭名稱包含在 meta 標籤中:

<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>

然後我們使用 JQuery 檢索 meta 標籤的值:

var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");

最後,讓我們使用這些值來設置我們的 XHR 頭部:

$(document).ajaxSend(function(e, xhr, options) {
    xhr.setRequestHeader(header, token);
});

4. 無狀態 Spring API

讓我們回顧一下無狀態 Spring API 被前端消費的案例。

正如我們在專門的文章中所解釋的,我們需要了解我們的無狀態 API 是否需要 CSRF 保護。

如果我們的無狀態 API 使用基於令牌的身份驗證,例如 JWT,則我們不需要 CSRF 保護,並且如之前所述,應將其禁用。

然而,如果我們的無狀態 API 使用會話 Cookie 身份驗證,則我們需要啓用 CSRF 保護,正如下一部分所展示的。

4.1. 後端配置

我們的無狀態 API 無法像 MVC 配置一樣添加 CSRF 令牌,因為它不生成任何 HTML 視圖。

在這種情況下,我們可以使用 CookieCsrfTokenRepository 將 CSRF 令牌發送到 cookie 中。

@Configuration
public class SecurityWithCsrfCookieConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.csrfTokenRepository
            (CookieCsrfTokenRepository.withHttpOnlyFalse()));
        return http.build();
    }
}

此配置將設置一個 XSRF-TOKEN Cookie 給前端。由於我們將 HTTP-only 標誌設置為 false,前端將可以使用 JavaScript 檢索該 Cookie。

4.2. 前端配置

使用 JavaScript,我們需要從 XSRF-TOKEN cookie 列表中搜索 XSRF-TOKEN 的值。

由於此列表存儲為字符串,我們可以使用以下正則表達式進行檢索:

const csrfToken = document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, '$1');

然後,我們必須將令牌發送到所有修改 API 狀態的 REST 請求:POST、PUT、DELETE 和 PATCH。

Spring 期望在 X-XSRF-TOKEN 標頭中接收到該令牌。

我們可以使用 JavaScript 的 Fetch API 簡單地設置它:

fetch(url, {
  method: 'POST',
  body: /* data to send */,
  headers: { 'X-XSRF-TOKEN': csrfToken },
})

5. CSRF 禁用測試

一切準備就緒後,我們來進行一些測試。

首先,我們嘗試在 CSRF 禁用時提交一個簡單的 POST 請求:

@ContextConfiguration(classes = { SecurityWithoutCsrfConfig.class, ...})
public class CsrfDisabledIntegrationTest extends CsrfAbstractIntegrationTest {

    @Test
    public void givenNotAuth_whenAddFoo_thenUnauthorized() throws Exception {
        mvc.perform(
          post("/foos").contentType(MediaType.APPLICATION_JSON)
            .content(createFoo())
          ).andExpect(status().isUnauthorized());
    }

    @Test 
    public void givenAuth_whenAddFoo_thenCreated() throws Exception {
        mvc.perform(
          post("/foos").contentType(MediaType.APPLICATION_JSON)
            .content(createFoo())
            .with(testUser())
        ).andExpect(status().isCreated()); 
    } 
}

在這裏,我們使用一個基類來持有通用的測試輔助邏輯——CsrfAbstractIntegrationTest

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public class CsrfAbstractIntegrationTest {
    @Autowired
    private WebApplicationContext context;

    @Autowired
    private Filter springSecurityFilterChain;

    protected MockMvc mvc;

    @Before
    public void setup() {
        mvc = MockMvcBuilders.webAppContextSetup(context)
          .addFilters(springSecurityFilterChain)
          .build();
    }

    protected RequestPostProcessor testUser() {
        return user("user").password("userPass").roles("USER");
    }

    protected String createFoo() throws JsonProcessingException {
        return new ObjectMapper().writeValueAsString(new Foo(randomAlphabetic(6)));
    }
}

需要注意的是,當用户擁有正確的安全憑據時,該請求已成功執行,無需提供任何額外信息。

這意味着攻擊者可以簡單地利用之前討論過的攻擊向量來破壞系統。

6. CSRF 保護測試

現在讓我們啓用 CSRF 保護,並查看差異:

@ContextConfiguration(classes = { SecurityWithCsrfConfig.class, ...})
public class CsrfEnabledIntegrationTest extends CsrfAbstractIntegrationTest {

    @Test
    public void givenNoCsrf_whenAddFoo_thenForbidden() throws Exception {
        mvc.perform(
          post("/foos").contentType(MediaType.APPLICATION_JSON)
            .content(createFoo())
            .with(testUser())
          ).andExpect(status().isForbidden());
    }

    @Test
    public void givenCsrf_whenAddFoo_thenCreated() throws Exception {
        mvc.perform(
          post("/foos").contentType(MediaType.APPLICATION_JSON)
            .content(createFoo())
            .with(testUser()).with(csrf())
          ).andExpect(status().isCreated());
    }
}

我們可以看到這個測試是如何使用不同的安全配置的——一個啓用了 CSRF 保護的配置。

現在,如果 POST 請求中未包含 CSRF 令牌,該請求將直接失敗,這自然意味着之前的攻擊不再可行。

此外,在測試中,csrf() 方法創建了一個 RequestPostProcessor,該方法會在請求中自動填充有效的 CSRF 令牌,用於測試目的。

7. 結論

在本文中,我們討論了幾個 CSRF 攻擊以及如何使用 Spring Security 來防止它們。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.