首頁 > 科技

SpringBoot 如何防禦 CSRF 攻擊?

2021-06-07 11:31:01

作者 | 江南一點雨 責編 | 歐陽姝黎

CSRF 就是跨域請求偽造,英文全稱是 Cross Site Request Forgery。

這是一種非常常見的 Web 攻擊方式,其實是很好防禦的,但是由於經常被很多開發者忽略,進而導致很多網站實際上都存在 CSRF 攻擊的安全隱患。

今天就來和大家聊一聊什麼是 CSRF 攻擊以及 CSRF 攻擊該如何防禦。


CSRF原理


想要防禦 CSRF 攻擊,那我們得先搞清楚什麼是 CSRF 攻擊,鬆哥通過下面一張圖,來和大家梳理 CSRF 攻擊流程:

其實這個流程很簡單:

  1. 假設使用者打開了招商銀行網上銀行網站,並且登入。
  2. 登入成功後,網上銀行會返回 Cookie 給前端,瀏覽器將 Cookie 儲存下來。
  3. 使用者在沒有登出網上銀行的情況下,在瀏覽器裡邊打開了一個新的選項卡,然後又去訪問了一個危險網站。
  4. 這個危險網站上有一個超連結,超連結的地址指向了招商銀行網上銀行。
  5. 使用者點選了這個超連結,由於這個超連結會自動攜帶上瀏覽器中儲存的 Cookie,所以使用者不知不覺中就訪問了網上銀行,進而可能給自己造成了損失。

CSRF 的流程大致就是這樣,接下來鬆哥用一個簡單的例子和小夥伴們展示一下 CSRF 到底是怎麼回事。


CSRF實踐


接下來,我創建一個名為 csrf-1 的 Spring Boot 項目,這個項目相當於我們上面所說的網上銀行網站,創建項目時引入 Web 和 Spring Security 依賴,如下:

創建成功後,方便起見,我們直接將 Spring Security 使用者名/密碼 配置在 application.properties 檔案中:

spring.security.user.name=javaboy
spring.security.user.password=123

然後我們提供兩個測試介面:

@RestController
public class HelloController {
@PostMapping("/transfer")
public void transferMoney(String name, Integer money) {
System.out.println("name = " + name);
System.out.println("money = " + money);
}
@GetMapping("/hello")
public String hello() {
return "hello";
}
}

假設 /transfer 是一個轉賬介面(這裡是假設,主要是給大家演示 CSRF 攻擊,真實的轉賬介面比這複雜)。

最後我們還需要配置一下 Spring Security,因為 Spring Security 中預設是可以自動防禦 CSRF 攻擊的,所以我們要把這個關閉掉:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable();
}
}

配置完成後,我們啟動 csrf-1 項目。

接下來,我們再創建一個 csrf-2 項目,這個項目相當於是一個危險網站,為了方便,這裡創建時我們只需要引入 web 依賴即可。

項目創建成功後,首先修改項目埠:

server.port=8081

然後我們在 resources/static 目錄下創建一個 hello.html ,內容如下:

<body>
<form action="http://localhost:8080/transfer" method="post">
<input type="hidden" value="javaboy" name="name">
<input type="hidden" value="10000" name="money">
<input type="submit" value="點選檢視美女圖片">
</form>
</body>

這裡有一個超連結,超連結的文字是點選檢視美女圖片,當你點選了超連結之後,會自動請求 http://localhost:8080/transfer 介面,同時隱藏域還攜帶了兩個參數。

配置完成後,就可以啟動 csrf-2 項目了。

接下來,使用者首先訪問 csrf-1 項目中的介面,在訪問的時候需要登入,使用者就執行了登入操作,訪問完整後,使用者並沒有執行登出操作,然後使用者訪問 csrf-2 中的頁面,看到了超連結,好奇這美女到底長啥樣,一點選,結果錢就被人轉走了。


CSRF防禦


先來說說防禦思路。

CSRF 防禦,一個核心思路就是在前端請求中,新增一個隨機數。

因為在 CSRF 攻擊中,黑客網站其實是不知道使用者的 Cookie 具體是什麼的,他是讓使用者自己傳送請求到網上銀行這個網站的,因為這個過程會自動攜帶上 Cookie 中的資訊。

所以我們的防禦思路是這樣:使用者在訪問網上銀行時,除了攜帶 Cookie 中的資訊之外,還需要攜帶一個隨機數,如果使用者沒有攜帶這個隨機數,則網上銀行網站會拒絕該請求。黑客網站誘導使用者點選超連結時,會自動攜帶上 Cookie 中的資訊,但是卻不會自動攜帶隨機數,這樣就成功的避免掉 CSRF 攻擊了。

Spring Security 中對此提供了很好的支援,我們一起來看下。

3.1 預設方案

Spring Security 中預設實際上就提供了 csrf 防禦,但是需要開發者做的事情比較多。

首先我們來創建一個新的 Spring Boot 工程,創建時引入 Spring Security、Thymeleaf 和 web 依賴。

項目創建成功後,我們還是在 application.properties 中配置使用者名/密碼:

spring.security.user.name=javaboy
spring.security.user.password=123

接下來,我們提供一個測試介面:

@Controller
public class HelloController {
@PostMapping("/hello")
@ResponseBody
public String hello() {
return "hello";
}
}

注意,這個測試介面是一個 POST 請求,因為預設情況下,GET、HEAD、TRACE 以及 OPTIONS 是不需要驗證 CSRF 攻擊的。

然後,我們在 resources/templates 目錄下,新建一個 thymeleaf 模版,如下:

<body>
<form action="/hello" method="post">
<input type="hidden" th:value="${_csrf.token}" th:name="${_csrf.parameterName}">
<input type="submit" value="hello">
</form>
</body>

注意,在傳送 POST 請求的時候,還額外攜帶了一個隱藏域,隱藏域的 key 是 ${_csrf.parameterName},value 則是 ${_csrf.token}。

這兩個值服務端會自動帶過來,我們只需要在前端渲染出來即可。

接下來給前端 hello.html 頁面新增一個控制器,如下:

@GetMapping("/hello")
public String hello2() {
return "hello";
}

新增完成後,啟動項目,我們訪問 hello 頁面,在訪問時候,需要先登入,登入成功之後,我們可以看到登入請求中也多了一個參數,如下:

可以看到,這裡也多了 _csrf 參數。

這裡我們用了 Spring Security 的預設登入頁面,如果大家使用自定義登入頁面,可以參考上面 hello.html 的寫法,通過一個隱藏域傳遞 _csrf 參數。

訪問到 hello 頁面之後,再去點選按鈕,就可以訪問到 hello 介面了。

小夥伴們可以自行嘗試在 hello.html 頁面中,去掉 _csrf 參數,看看訪問 hello 介面的效果。

這是 Spring Security 中預設的方案,通過 Model 將相關的資料帶到前端來。

如果你的項目是前後端不分項目,這種方案就可以了,如果你的項目是前後端分離項目,這種方案很明顯不夠用。

3.2 前後端分離方案

如果是前後端分離項目,Spring Security 也提供瞭解決方案。

這次不是將 _csrf 放在 Model 中返回前端了,而是放在 Cookie 中返回前端,配置方式如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}

有小夥伴可能會說放在 Cookie 中不是又被黑客網站盜用了嗎?其實不會的,大家注意如下兩個問題:

  1. 黑客網站根本不知道你的 Cookie 裡邊存的啥,他也不需要知道,因為 CSRF 攻擊是瀏覽器自動攜帶上 Cookie 中的資料的。
  2. 我們將服務端生成的隨機數放在 Cookie 中,前端需要從 Cookie 中自己提取出來 _csrf 參數,然後拼接成參數傳遞給後端,單純的將 Cookie 中的資料傳到服務端是沒用的。

理解透了上面兩點,你就會發現 _csrf 放在 Cookie 中是沒有問題的,但是大家注意,配置的時候我們通過 withHttpOnlyFalse 方法獲取了 CookieCsrfTokenRepository 的例項,該方法會設定 Cookie 中的 HttpOnly 屬性為 false,也就是允許前端通過 js 操作 Cookie(否則你就沒有辦法獲取到 _csrf)。

配置完成後,重啟項目,此時我們就發現返回的 Cookie 中多了一項:

接下來,我們通過自定義登入頁面,來看看前端要如何操作。

首先我們在 resources/static 目錄下新建一個 html 頁面叫做 login.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/jquery.min.js"></script>
<script src="js/jquery.cookie.js"></script>
</head>
<body>
<div>
<input type="text" id="username">
<input type="password" id="password">
<input type="button" value="登入" id="loginBtn">
</div>
<script>
$("#loginBtn").click(function () {
let _csrf = $.cookie('XSRF-TOKEN');
$.post('/login.html',{username:$("#username").val(),password:$("#password").val(),_csrf:_csrf},function (data) {
alert(data);
})
})
</script>
</body>
</html>

這段 html 我給大家解釋下:

  1. 首先引入 jquery 和 jquery.cookie ,方便我們一會操作 Cookie。
  2. 定義三個 input,前兩個是使用者名和密碼,第三個是登入按鈕。
  3. 點選登入按鈕之後,我們先從 Cookie 中提取出 XSRF-TOKEN,這也就是我們要上傳的 csrf 參數。
  4. 通過一個 POST 請求執行登入操作,注意攜帶上 _csrf 參數。

服務端我們也稍作修改,如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/js/**");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.successHandler((req,resp,authentication)->{
resp.getWriter().write("success");
})
.permitAll()
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}

一方面這裡給 js 檔案放行。

另一方面配置一下登入頁面,以及登入成功的回撥,這裡簡單期間,登入成功的回撥我就給一個字元串就可以了。大家感興趣的話,可以檢視本系列前面文章,有登入成功後回撥的詳細解釋。

OK,所有事情做完之後,我們訪問 login.html 頁面,輸入使用者名密碼進行登入,結果如下:

可以看到,我們的 _csrf 配置已經生效了。

小夥伴們可以自行嘗試從登入參數中去掉 _csrf,然後再看看效果。


小結


好了,今天主要和小夥伴們介紹了 csrf 攻擊以及如何防禦的問題。大家看到,csrf 攻擊主要是藉助了瀏覽器預設傳送 Cookie 的這一機制,所以如果你的前端是 App、小程式之類的應用,不涉及瀏覽器應用的話,其實可以忽略這個問題,如果你的前端包含瀏覽器應用的話,這個問題就要認真考慮了。

好了 ,本文就說到這裡,本文相關案例我已經上傳到 GitHub ,大家可以自行下載:https://github.com/lenve/spring-security-samples


生於2001年的《程式設計師》曾陪伴了無數開發者成長,影響了一代又一代的中國技術人。時隔20年,《新程式設計師》帶著全球技術大師深邃思考、優秀開發者技術創造等深度內容回來了!同時將全方位為所有開發者呈現國內外核心技術生態體系全景圖。掃描下方小程式碼即可立即訂閱!


IT145.com E-mail:sddin#qq.com