想要防禦CSRF 攻擊,那我們需要先搞清楚什麼是CSRF 攻擊,透過下面圖例來和大家梳理CSRF 攻擊流程:
其實這個流程很簡單:
1.假設使用者開啟了招商網路銀行網站,並且登入。
2.登入成功後,網路銀行會回到Cookie給前端,瀏覽器將Cookie儲存下來。
3.用戶在沒有登出網路銀行的情況下,在瀏覽器裡打開了一個新的選項卡,然後又去訪問了一個危險網站。
4.這個危險網站上有一個超鏈接,超鏈接的地址指向了招商網上銀行。
4.用戶點擊了這個鏈接,由於這個超鏈接會自動攜帶上瀏覽器中保存的Cookie,所以用戶不知不覺中就訪問了網上銀行,進而可能給自己造成了損失。
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 攻擊中,駭客網站其實是不知道用戶的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攻擊的流程及原理是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!