从第一个示例中,配置类中一直存在这样一行代码:
http.csrf(csrfCustomizer -> csrfCustomizer.ignoringRequestMatchers("*"))
Spring Security 6.* 版本之前是:
http.csrf().disable();
它们的作用都是相同的,用来关闭 CSRF 防护。如果没有上述代码,将导致用户无法被认证。如下图:
上图从 Network 查看,login 请求返回了 200,但是就是没有跳转到登录页面,这是因为我们开启了 CSRF,但有没有对 CSRF 进行处理导致的。如下图:
下面将介绍如何来处理 CSRF 以及简单介绍 CSRF。
下面是来自维基百科的解释:
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF,是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求是发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
假如一家银行用以执行转账操作的URL地址如下: https://bank.example.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那么,一个恶意攻击者可以在另一个网站上放上 <img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman" />
如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。
透过例子能够看出,攻击者并不能通过CSRF攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户的浏览器,让其以用户的名义执行操作。
通俗的说:CSRF 就是别的网站非法获取我们网站 Cookie 值,我们项目服务器是无法区分到底是不是我们的客户端,只有请求中有 Cookie,认为是自己的客户端,所以这个时候就出现了 CSRF。
从 Spring Security4 开始 CSRF 防护默认开启。默认会拦截请求,进行 CSRF 处理。CSRF 为了保证不是其他第三方网站访问,要求访问时携带参数名为 _csrf 值为 token(token在服务端产生) 的内容,如果token 和服务端的 token 匹配成功,则正常访问。
在项目 resources 下新建 templates 文件夹,并在文件夹中新建 login.html 页面。如下:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>自定义登录</title> </head> <body> <!-- 显示错误信息 --> <div id="errorInfo" style="color:red;display:none;">用户名/密码错误</div> <script> if(location.href.indexOf("?error") !== -1) { document.getElementById("errorInfo").style.display = "block"; } </script> <form action="/login" method="post"> <!-- 这里的 input 用于存放从服务器拉取的 token,请注意 --> <input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/> <p> 用户名:<input type="text" name="username" placeholder="请输入用户名" /> </p> <p> 密码:<input type="password" name="password" placeholder="请输入密码" /> </p> <p> <input type="submit" value="登录"/> </p> </form> </body> </html>
关键代码为:
<input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/>
在配置类中注释掉 CSRF 防护失效相关配置,如下:
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { System.out.println("securityFilterChain()"); // csrfCustomizer.ignoringRequestMatchers(*) 表示所有请求地址都不使用 csrf http/*.csrf(csrfCustomizer -> csrfCustomizer.ignoringRequestMatchers("*"))*/ // authorizeHttpRequests 用于配置请求授权 .authorizeHttpRequests(authorize -> authorize .requestMatchers("/sys/**").hasRole("admin") .anyRequest().authenticated()) //... .httpBasic(withDefaults()); return http.build(); }
重启项目,使用浏览器访问登录页面,且通过调试工具查看登录表单下面 input 的值,如下图:
上图中,我们已经成功从服务端获取到 token 值。此时,再次输入用户名和密码,点击登录就登录成功了,如下图:
上图中,我们发起的 login 请求携带了 _csrf 参数,因此登录成功。
Spring Security 通过以下流程实现 CSRF 防护:
(1)CSRF 令牌的生成与注入:当服务器加载登录页面(即配置的 loginPage 路径,默认是 /login)时,会自动创建一个 CSRF 令牌对象。这个对象会被存入请求的作用域中,键名为 "_csrf"。在页面渲染阶段,模板引擎会将表单中的占位符 ${_csrf.token} 动态替换为服务器生成的实际令牌字符串。例如,Thymeleaf 模板会这样处理:
<form action="/login" method="post"> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> <!-- 其他表单字段 --> </form>
(2)令牌的验证流程:当用户提交登录表单时,表单数据中会包含这个 CSRF 令牌。服务器接收到请求后,会提取客户端提交的令牌,并与服务器端存储的令牌进行比对。只有当两者完全匹配时,服务器才会认为这个请求是来自受信任的客户端,从而继续处理登录逻辑;否则,请求会被拦截并返回 403 Forbidden 错误。
注意,这种机制有效防止了攻击者通过诱导用户在已登录状态下执行恶意操作的风险,确保了请求的合法性和安全性。