Csrf以及在SpringSecurity中的防护

csrf(cross site request forgery)跨站请求伪造与springSecurity解决方案

是什么

CSRF 利用了系统对登录期用户的信任,使得用户执行了某些并非意愿的操作从而造成损失
案例:
1 你在某个商城网站登录了,服务端返回一个记录你登录状态的cookie
2 这个时候你桌面突然出来一个广告页面(点击进入xx网页游戏),你点击了以后,跳入B网站,同时B网站利用你网站记录的session信息,发送购买POST请求给商城网站,下单充值游戏
3 商城网站看到这是你登录的session,是有效的,执行这次请求,为骗子充值
4 你莫名其妙发现自己明明什么没有做,却有充值订单和钱被扣除

如何防御

使用POST请求时,确实避免了如img、script、iframe等标签自动发起GET请求的问题,但这并不能杜绝CSRF攻击的发生。一些恶意网站会通过表单的形式构造攻击请求


image.png

HTTP Refer

HTTP Referer是header的一部分,当浏览器向web服务器发送请求的时候,会带上Referer,通过验证Referer,可以判断请求的合法性,如果Referer是其他网站的话,就有可能是CSRF攻击,则拒绝该请求

        //获取header中的referrer
        String refer = request.getHeader("referer");
        //获取当前网站地址
        String sourceAddress = request.getScheme())+"://"+request.getServerName();
        if(Stringutils.isBlank(refer)||refer.lastIndexOf(String.valueOf(sourceAddress))==0){
            return false;
        }

缺点
这种方式简单便捷,但并非完全可靠。除前面提到的部分浏览器可以篡改 HTTP Referer外,如果用户在浏览器中设置了不被跟踪,那么HTTP Referer字段就不会自动添加,当合法用户访问时,系统会认为是CSRF攻击,从而拒绝访问

我们知道正常的页面跳转,浏览器都会自动带上Referer,那么现在的问题就变成了什么情况下浏览器会不带Referer?可以大致总结为3种情况:

  1. Refer的作用是指一个请求是从哪里链接过来,那么当一个请求并不是由链接触发产生的,那么自然也就不需要指定这个请求的链接来源,比如直接在浏览器的地址栏中输入一个资源的URL地址(GET),那么这种请求是不会包含Referer字段的,因为这是一个“凭空产生”的HTTP请求,并不是从一个地方链接过去的
  2. 跨协议间提交请求。常见的协议:ftp://,http://,https://,file://,javascript:,data:.最简单的情况就是我们在本地打开一个HTML页面,这个时候浏览器地址栏是file://开头的,如果这个HTML页面向任何http站点提交请求的话,这些请求的Referer都是空的。那么我们接下来可以利用data:协议来构造一个自动提交的CSRF攻击。当然这个协议是IE不支持的,我们可以换用javascript:
  3. 用户在浏览器中设置了不被跟踪,那么HTTP Referer字段就不会自动添加

Csrf Token

用户登录时,系统发放一个CsrfToken值,用户携带该CsrfToken值与用户名、密码等参数完成登录。系统记录该会话的 CsrfToken 值,之后在用户的任何请求中,都必须带上该CsrfToken值,并由系统进行校验。
这种方法需要与前端配合,包括存储CsrfToken值,以及在任何请求中(包括表单和Ajax)携带CsrfToken值。安全性相较于HTTP Referer提高很多,如果都是XMLHttpRequest,则可以统一添加CsrfToken值;但如果存在大量的表单和a标签,就会变得非常烦琐。

SpringSecurity中使用Csrf Token

Spring Security通过注册一个CsrfFilter来专门处理CSRF攻击,在Spring Security中,CsrfToken是一个用于描述Token值,以及验证时应当获取哪个请求参数或请求头字段的接口


image.png
public interface CsrfToken extends Serializable {
    String getHeaderName();
    String getParameterName();
    String getToken();
}
//CsrfTokenRepository则定义了如何生成、保存以及加载CsrfToken。
public interface CsrfTokenRepository {
    CsrfToken generateToken(HttpServletRequest request);
    void saveToken(CsrfToken token, HttpServletRequest request,
                   HttpServletResponse response);
    CsrfToken loadToken(HttpServletRequest request);
}

在默认情况下,Spring Security加载的是一个HttpSessionCsrfTokenRepository
HttpSessionCsrfTokenRepository 将 CsrfToken 值存储在 HttpSession 中,并指定前端把CsrfToken 值放在名为“_csrf”的请求参数或名为“X-CSRF-TOKEN”的请求头字段里(可以调用相应的设置方法来重新设定)。校验时,通过对比HttpSession内存储的CsrfToken值与前端携带的CsrfToken值是否一致,便能断定本次请求是否为CSRF攻击。

<input type='hidden' name='${_csrf.parameterName}' value='${_csrf.token}'>

这种方式灵活性不足
Spring Security还提供了另一种方式,即CookieCsrfTokenRepository
CookieCsrfTokenRepository 是一种更加灵活可行的方案,它将 CsrfToken 值存储在用户的cookie内。减少了服务器HttpSession存储的内存消耗,并且当用cookie存储CsrfToken值时,前端可以用JavaScript读取(需要设置该cookie的httpOnly属性为false),而不需要服务器注入参数,在使用方式上更加灵活。

存储在cookie中是不可以被CSRF利用的,cookie 只有在同域的情况下才能被读取,所以杜绝了第三方站点跨域获取 CsrfToken 值的可能。CSRF攻击本身是不知道cookie内容的,只是利用了当请求自动携带cookie时可以通过身份验证的漏洞。但服务器对 CsrfToken 值的校验并非取自 cookie,而是需要前端手动将CsrfToken值作为参数携带在请求里

下面是csrfFilter的过滤过程

@Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
        request.setAttribute(HttpServletResponse.class.getName(), response);
                
                //获取到cookie中的csrf Token(CookieTokenRepository)或者从session中获取(HttpSessionCsrfTokenRepository)
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        final boolean missingToken = csrfToken == null;
                //加载不到,则证明请求是首次发起的,应该生成并保存一个新的 CsrfToken 值
        if (missingToken) {
            csrfToken = this.tokenRepository.generateToken(request);
            this.tokenRepository.saveToken(csrfToken, request, response);
        }
        request.setAttribute(CsrfToken.class.getName(), csrfToken);
        request.setAttribute(csrfToken.getParameterName(), csrfToken);

                //排除部分不需要验证CSRF攻击的请求方法(默认忽略了GET、HEAD、TRACE和OPTIONS)
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            filterChain.doFilter(request, response);
            return;
        }

                //实际的token从header或者parameter中获取
        String actualToken = request.getHeader(csrfToken.getHeaderName());
        if (actualToken == null) {
            actualToken = request.getParameter(csrfToken.getParameterName());
        }
        if (!csrfToken.getToken().equals(actualToken)) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Invalid CSRF token found for "
                        + UrlUtils.buildFullRequestUrl(request));
            }
            if (missingToken) {
                this.accessDeniedHandler.handle(request, response,
                        new MissingCsrfTokenException(actualToken));
            }
            else {
                this.accessDeniedHandler.handle(request, response,
                        new InvalidCsrfTokenException(csrfToken, actualToken));
            }
            return;
        }

        filterChain.doFilter(request, response);
    }

我们在日常使用中,可以采用header或者param的方式添加csrf_token,下面示范从cookie中获取token

<form action="/executeLogin" method="post">
<p>Sign in to continue</p>
<div class="lowin-group">
    <label>用户名 <a href="#" class="login-back-link">Sign in?</a></label>
    <input type="text" name="username" class="lowin-input">
</div>
<div class="lowin-group password-group">
    <label>密码 <a href="#" class="forgot-link">Forgot Password?</a></label>
    <input type="password" name="password" class="lowin-input">
</div>
<div class="lowin-group">
    <label>验证码</label>
    <input type="text" name="kaptcha" class="lowin-input">
    <img src="/kaptcha.jpg" alt="kaptcha" height="50px" width="150px" style="margin-left: 20px">
</div>
<div class="lowin-group">
    <label>记住我</label>
    <input name="remember-me" type="checkbox" value="true" />
</div>
<input type="hidden" name="_csrf">
<input class="lowin-btn login-btn" type="submit">
</form>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>   
<script>
    $(function () {
        var aCookie = document.cookie.split("; ");
        console.log(aCookie);
        for (var i=0; i < aCookie.length; i++)
        {
            var aCrumb = aCookie[i].split("=");
            if ("XSRF-TOKEN" == aCrumb[0])
                $("input[name='_csrf']").val(aCrumb[1]);
        }
    });
</script>

注意事项

springSecurity配置了默认放行, 不需要通过csrfFilter过滤器检测的http访问方式

    private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
        private final HashSet<String> allowedMethods = new HashSet<>(
                Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
        @Override
        public boolean matches(HttpServletRequest request) {
            return !this.allowedMethods.contains(request.getMethod());
        }
    }

之所以会有上面默认的GET,HEAD,TRACE,OPTIONS方式,是因为

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

  2. 如果这个http请求是通过post请求发起的, 那么spring security是默认拦截这类请求的
    因为这类请求是带有更新服务器资源的危险操作,如果恶意第三方可以通过劫持session id来更新 服务器资源,那会造成服务器数据被非法的篡改,所以这类请求是会被Spring security拦截的,在默认的情况下,spring security是启用csrf 拦截功能的,这会造成,在跨域的情况下,post方式提交的请求都会被拦截无法被处理(包括合理的post请求),前端发起的post请求后端无法正常 处理,虽然保证了跨域的安全性,但影响了正常的使用,如果关闭csrf防护功能,虽然可以正常处理post请求,但是无法防范通过劫持session id的非法的post请求,所以spring security为了正确的区别合法的post请求,采用了token的机制 。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,809评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,189评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,290评论 0 359
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,399评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,425评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,116评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,710评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,629评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,155评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,261评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,399评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,068评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,758评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,252评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,381评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,747评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,402评论 2 358