Spring security和CSRF相关问题

什么是CSRF

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

一个典型的CSRF攻击有着如下的流程:

  • 受害者登录a.com,并保留了登录凭证(Cookie)。
  • 攻击者引诱受害者访问了b.com。
  • b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie。
  • a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
  • a.com以受害者的名义执行了act=xx。
  • 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。

几种常见的攻击类型

GET类型的CSRF

GET类型的CSRF利用非常简单,只需要一个HTTP请求,一般会这样利用:

 ![](https://awps-assets.meituan.net/mit-x/blog-images-bundle-2018b/ff0cdbee.example/withdraw?amount=10000&for=hacker)
 

在受害者访问含有这个img的页面后,浏览器会自动向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker发出一次HTTP请求。bank.example就会收到包含受害者登录信息的一次跨域请求。

POST类型的CSRF

这种类型的CSRF利用起来通常使用的是一个自动提交的表单,如:

 <form action="http://bank.example/withdraw" method=POST>
    <input type="hidden" name="account" value="xiaoming" />
    <input type="hidden" name="amount" value="10000" />
    <input type="hidden" name="for" value="hacker" />
</form>
<script> document.forms[0].submit(); </script> 

访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作。

POST类型的攻击通常比GET要求更加严格一点,但仍并不复杂。任何个人网站、博客,被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许POST上面。

链接类型的CSRF

链接类型的CSRF并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:

  <a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
  重磅消息!!
  <a/>

由于之前用户登录了信任的网站A,并且保存登录状态,只要用户主动访问上面的这个PHP页面,则表示攻击成功。

SpringSecurity问题

        开启csrf后,无法访问资源。页面console.log(403)。在跨域的情况下,post方式提交的请求都会被拦截无法被处理(包括合理的post请求),前端发起的post请求后端无法正常 处理。但 在 RESTFul 开发的时候,这个情况就没有办法避免,因为我们的 API 会暴露给不同的用户,用户可能也会使用不同的 IP 地址,尤其用户可能还部署了多个服务器的情况下。因此,我们在 Spring 安全配置下,需要禁用 CSRF。

SpringSecurity中的关键点

1.如果这个http请求是通过get方式发起的请求,意味着它只是访问服务器 的资源,仅仅只是查询,没有更新服务器的资源,所以对于这类请求,spring security的防御策略是允许的;

2.如果这个http请求是通过post请求发起的, 那么spring security是默认拦截这类请求的,因为这类请求是带有更新服务器资源的危险操作,如果恶意第三方可以通过劫持session id来更新 服务器资源,那会造成服务器数据被非法的篡改,所以这类请求是会被Spring security拦截的,在默认的情况下,spring security是启用csrf 拦截功能的。

SpringSecurity为了正确的区别合法的跨域Post请求,采用了Token机制。为了保护MVC应用程序,Spring为每个生成的视图添加了一个CSRF Token。在http发送(PATCH、POST、PUT 和 DELETE – 而不是 GET)请求时,将Token上传到服务器。这可以保护我们的应用程序免受 CSRF 攻击,因为攻击者无法从自己的页面获取此令牌。需要在请求接口的时候加入csrfToken才能正常访问。

CSRF Token的防护策略分为三个步骤:

1. 将CSRF Token输出到页面中

首先,用户打开页面的时候,服务器需要给这个用户生成一个Token,该Token通过加密算法对数据进行加密,一般Token都包括随机字符串和时间戳的组合,显然在提交时Token不能再放在Cookie中了,否则又会被攻击者冒用。因此,为了安全起见Token最好还是存在服务器的Session中,之后在每次页面加载时,使用JS遍历整个DOM树,对于DOM中所有的a和form标签后加入Token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的HTML代码,这种方法就没有作用,还需要程序员在编码时手动添加Token。

2. 页面提交的请求携带这个Token

对于GET请求,Token将附在请求地址之后,这样URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上:

  <input type=”hidden” name=”csrftoken” value=”tokenvalue”/>

这样,就把Token以参数的形式加入请求了。

3. 服务器验证Token是否正确

当用户从客户端得到了Token,再次提交给服务器的时候,服务器需要判断Token的有效性,验证过程是先解密Token,对比加密字符串以及时间戳,如果加密字符串一致且时间未过期,那么这个Token就是有效的。

前后端分离方案

如果是前后端分离项目,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,然后再看看效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值