SpringBoot 防禦 CSRF 攻擊的流程及原理解析
CSRF 就是跨域請求偽造,英文全稱是 Cross Site Request Forgery。
這是一種非常常見的Web攻擊方式,其實是很好防禦的,但是由於經常被很多開發者忽略,進而導致很多網站實際上都存在 CSRF 攻擊的安全隱患。
今天和大傢聊一聊什麼是 CSRF 攻擊以及 CSRF 攻擊該如何防禦。
CSRF 原理
想要防禦 CSRF 攻擊,那我們需要先搞清楚什麼是 CSRF 攻擊,通過下面圖例來和大傢梳理 CSRF 攻擊流程:
其實這個流程很簡單:
1.假設用戶打開瞭招商網上銀行網站,並且登錄。
2.登錄成功後,網上銀行會返回Cookie給前端,瀏覽器將Cookie保存下來。
3.用戶在沒有登出網上銀行的情況下,在瀏覽器裡打開瞭一個新的選項卡,然後又去訪問瞭一個危險網站。
4.這個危險網站上有一個超鏈接,超鏈接的地址指向瞭招商網上銀行。
4.用戶點擊瞭這個鏈接,由於這個超鏈接會自動攜帶上瀏覽器中保存的Cookie,所以用戶不知不覺中就訪問瞭網上銀行,進而可能給自己造成瞭損失。
CSRF的流程大致就是這樣,接下來用一個簡單的例子展示一下CSRF到底是怎麼一回事。
CSRF實踐
1.我創建一個名為 csrf-mry 的 Spring Boot 項目,這個項目相當於我們上面所說的網上銀行網站,創建項目時引入 Web 和 Spring Security依賴,如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
2.創建成功後,方便起見,我們直接將 Spring Security 用戶名/密碼 配置在 application.properties 文件中:
server.port= 8866 spring.security.user.name=javaboy spring.security.user.password=123
3.然後我們提供兩個測試接口
package com.mry.csrf.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class CsrfDemoController { @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 攻擊,真實的轉賬接口比這復雜)。
4.我們還需要配置一下 Spring Security,因為 Spring Security 中默認是可以自動防禦 CSRF 攻擊的,所以我們要把這個關閉掉。
package com.mry.csrf.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated() .and() .formLogin() .and() .csrf() .disable(); } }
配置完成後,我們啟動 csrf-simulate-web 項目。
5.我們再創建一個 csrf-loophole-web 項目,這個項目相當於是一個危險網站,為瞭方便,這裡創建時我們隻需要引入 web 依賴即可。
項目創建成功後,首先修改項目端口:
server.port= 8855
6.然後我們在 resources/static 目錄下創建一個 hello.html ,內容如下。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="http://localhost:8866/transfer" method="post"> <input type="hidden" value="javaboy" name="name"> <input type="hidden" value="10000" name="money"> <input type="submit" value="點擊查看美女圖片"> </form> </body> </html>
這裡有一個超鏈接,超鏈接的文本是點擊查看美女圖片,當你點擊瞭超鏈接之後,會自動請求 http://localhost:8866/transfer 接口,同時隱藏域還攜帶瞭兩個參數。
配置完成後,就可以啟動 csrf-loophole-web 項目瞭。
接下來,用戶首先訪問 csrf-simulate-web 項目中的接口,在訪問的時候需要登錄,用戶就執行瞭登錄操作,訪問完整後,用戶並沒有執行登出操作,然後用戶訪問 csrf-loophole-web 中的頁面,看到瞭超鏈接,好奇這美女到底長啥樣,一點擊,結果錢就被人轉走瞭。
CSRF防禦
先來說說防禦思路。
CSRF 防禦,一個核心思路就是在前端請求中,添加一個隨機數。
因為在 CSRF 攻擊中,黑客網站其實是不知道用戶的 Cookie 具體是什麼的,他是讓用戶自己發送請求到網上銀行這個網站的,因為這個過程會自動攜帶上 Cookie 中的信息。
所以我們的防禦思路是這樣:用戶在訪問網上銀行時,除瞭攜帶 Cookie 中的信息之外,還需要攜帶一個隨機數,如果用戶沒有攜帶這個隨機數,則網上銀行網站會拒絕該請求。黑客網站誘導用戶點擊超鏈接時,會自動攜帶上 Cookie 中的信息,但是卻不會自動攜帶隨機數,這樣就成功的避免掉 CSRF 攻擊瞭。
Spring Security 中對此提供瞭很好的支持,我們一起來看下。
前後端不分離方案
Spring Security 中默認實際上就提供瞭 csrf 防禦,但是需要開發者做的事情比較多。
首先我們來創建一個新的 Spring Boot 工程,創建時引入 Spring Security、Thymeleaf 和 web 依賴。
1.pom信息
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
2.項目創建成功後,我們還是在 application.properties 中配置用戶名/密碼
spring.security.user.name=mry spring.security.user.password=123456
3.接下來,我們提供一個測試接口
package com.mry.csrf.controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; @RestController public class SecurityCsrfController { @PostMapping("/hello") @ResponseBody public String hello() { return "hello"; } }
註意,這個測試接口是一個 POST 請求,因為默認情況下,GET、HEAD、TRACE 以及 OPTIONS 是不需要驗證 CSRF 攻擊的。
4.然後,我們在 resources/templates 目錄下,新建一個 thymeleaf 模版
<!DOCTYPE html> <!--導入thymeleaf的名稱空間--> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="/hello" method="post"> <input type="hidden" th:value="${_csrf.token}" th:name="${_csrf.parameterName}"> <input type="submit" value="hello"> </form> </body> </html>
註意,在發送 POST 請求的時候,還額外攜帶瞭一個隱藏域,隱藏域的 key 是 ${_csrf.parameterName},value 則是 ${_csrf.token}。
這兩個值服務端會自動帶過來,我們隻需要在前端渲染出來即可。
5.接下來給前端 hello.html 頁面添加一個控制器
@GetMapping("/hello") public String hello2() { return "hello"; }
6.添加完成後,啟動項目,我們訪問 hello 頁面,在訪問時候,需要先登錄,登錄成功之後,我們可以看到登錄請求中也多瞭一個參數
這裡我們用瞭 Spring Security 的默認登錄頁面,如果大傢使用自定義登錄頁面,可以參考上面 hello.html 的寫法,通過一個隱藏域傳遞 _csrf 參數。
訪問到 hello 頁面之後,再去點擊【hello】按鈕,就可以訪問到 hello 接口瞭。
這是 Spring Security 中默認的方案,通過 Model 將相關的數據帶到前端來。
如果你的項目是前後端不分項目,這種方案就可以瞭,如果你的項目是前後端分離項目,這種方案很明顯不夠用。
前後端分離方案
如果是前後端分離項目,Spring Security 也提供瞭解決方案。
這次不是將 _csrf 放在 Model 中返回前端瞭,而是放在 Cookie 中返回前端,配置方式如下:
package com.mry.csrf.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; @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="https://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script> <script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.min.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 參數。
服務端我們也稍作修改,如下:
package com.mry.csrf.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/static/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 配置已經生效瞭。
到此這篇關於SpringBoot 如何防禦 CSRF 攻擊的文章就介紹到這瞭,更多相關SpringBoot 防禦 CSRF 攻擊內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Spring Boot Admin的使用詳解(Actuator監控接口)
- java SpringSecurity使用詳解
- Spring Boot Admin 快速入門詳解
- SpringBoot中配置SSL的同時支持http和https訪問
- SpringBoot+WebSocket實現多人在線聊天案例實例