Csrf以及在SpringSecurity中的防護(hù)

csrf(cross site request forgery)跨站請(qǐng)求偽造與springSecurity解決方案

是什么

CSRF 利用了系統(tǒng)對(duì)登錄期用戶的信任烫饼,使得用戶執(zhí)行了某些并非意愿的操作從而造成損失
案例:
1 你在某個(gè)商城網(wǎng)站登錄了媒峡,服務(wù)端返回一個(gè)記錄你登錄狀態(tài)的cookie
2 這個(gè)時(shí)候你桌面突然出來一個(gè)廣告頁(yè)面(點(diǎn)擊進(jìn)入xx網(wǎng)頁(yè)游戲)敢艰,你點(diǎn)擊了以后其骄,跳入B網(wǎng)站颓芭,同時(shí)B網(wǎng)站利用你網(wǎng)站記錄的session信息膊毁,發(fā)送購(gòu)買POST請(qǐng)求給商城網(wǎng)站丰歌,下單充值游戲
3 商城網(wǎng)站看到這是你登錄的session惰帽,是有效的橡淆,執(zhí)行這次請(qǐng)求召噩,為騙子充值
4 你莫名其妙發(fā)現(xiàn)自己明明什么沒有做,卻有充值訂單和錢被扣除

如何防御

使用POST請(qǐng)求時(shí)逸爵,確實(shí)避免了如img具滴、script、iframe等標(biāo)簽自動(dòng)發(fā)起GET請(qǐng)求的問題师倔,但這并不能杜絕CSRF攻擊的發(fā)生构韵。一些惡意網(wǎng)站會(huì)通過表單的形式構(gòu)造攻擊請(qǐng)求


image.png

HTTP Refer

HTTP Referer是header的一部分,當(dāng)瀏覽器向web服務(wù)器發(fā)送請(qǐng)求的時(shí)候趋艘,會(huì)帶上Referer疲恢,通過驗(yàn)證Referer,可以判斷請(qǐng)求的合法性瓷胧,如果Referer是其他網(wǎng)站的話显拳,就有可能是CSRF攻擊,則拒絕該請(qǐng)求

        //獲取header中的referrer
        String refer = request.getHeader("referer");
        //獲取當(dāng)前網(wǎng)站地址
        String sourceAddress = request.getScheme())+"://"+request.getServerName();
        if(Stringutils.isBlank(refer)||refer.lastIndexOf(String.valueOf(sourceAddress))==0){
            return false;
        }

缺點(diǎn)
這種方式簡(jiǎn)單便捷搓萧,但并非完全可靠杂数。除前面提到的部分瀏覽器可以篡改 HTTP Referer外宛畦,如果用戶在瀏覽器中設(shè)置了不被跟蹤,那么HTTP Referer字段就不會(huì)自動(dòng)添加揍移,當(dāng)合法用戶訪問時(shí)次和,系統(tǒng)會(huì)認(rèn)為是CSRF攻擊,從而拒絕訪問

我們知道正常的頁(yè)面跳轉(zhuǎn)羊精,瀏覽器都會(huì)自動(dòng)帶上Referer斯够,那么現(xiàn)在的問題就變成了什么情況下瀏覽器會(huì)不帶Referer?可以大致總結(jié)為3種情況:

  1. Refer的作用是指一個(gè)請(qǐng)求是從哪里鏈接過來喧锦,那么當(dāng)一個(gè)請(qǐng)求并不是由鏈接觸發(fā)產(chǎn)生的读规,那么自然也就不需要指定這個(gè)請(qǐng)求的鏈接來源,比如直接在瀏覽器的地址欄中輸入一個(gè)資源的URL地址(GET),那么這種請(qǐng)求是不會(huì)包含Referer字段的燃少,因?yàn)檫@是一個(gè)“憑空產(chǎn)生”的HTTP請(qǐng)求束亏,并不是從一個(gè)地方鏈接過去的
  2. 跨協(xié)議間提交請(qǐng)求。常見的協(xié)議:ftp://,http://,https://,file://,javascript:,data:.最簡(jiǎn)單的情況就是我們?cè)诒镜卮蜷_一個(gè)HTML頁(yè)面阵具,這個(gè)時(shí)候?yàn)g覽器地址欄是file://開頭的碍遍,如果這個(gè)HTML頁(yè)面向任何http站點(diǎn)提交請(qǐng)求的話,這些請(qǐng)求的Referer都是空的阳液。那么我們接下來可以利用data:協(xié)議來構(gòu)造一個(gè)自動(dòng)提交的CSRF攻擊怕敬。當(dāng)然這個(gè)協(xié)議是IE不支持的,我們可以換用javascript:
  3. 用戶在瀏覽器中設(shè)置了不被跟蹤帘皿,那么HTTP Referer字段就不會(huì)自動(dòng)添加

Csrf Token

用戶登錄時(shí)东跪,系統(tǒng)發(fā)放一個(gè)CsrfToken值,用戶攜帶該CsrfToken值與用戶名鹰溜、密碼等參數(shù)完成登錄虽填。系統(tǒng)記錄該會(huì)話的 CsrfToken 值,之后在用戶的任何請(qǐng)求中曹动,都必須帶上該CsrfToken值斋日,并由系統(tǒng)進(jìn)行校驗(yàn)。
這種方法需要與前端配合墓陈,包括存儲(chǔ)CsrfToken值恶守,以及在任何請(qǐng)求中(包括表單和Ajax)攜帶CsrfToken值。安全性相較于HTTP Referer提高很多贡必,如果都是XMLHttpRequest熬的,則可以統(tǒng)一添加CsrfToken值;但如果存在大量的表單和a標(biāo)簽赊级,就會(huì)變得非常煩瑣。

SpringSecurity中使用Csrf Token

Spring Security通過注冊(cè)一個(gè)CsrfFilter來專門處理CSRF攻擊岔绸,在Spring Security中理逊,CsrfToken是一個(gè)用于描述Token值橡伞,以及驗(yàn)證時(shí)應(yīng)當(dāng)獲取哪個(gè)請(qǐng)求參數(shù)或請(qǐng)求頭字段的接口


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);
}

在默認(rèn)情況下兑徘,Spring Security加載的是一個(gè)HttpSessionCsrfTokenRepository
HttpSessionCsrfTokenRepository 將 CsrfToken 值存儲(chǔ)在 HttpSession 中,并指定前端把CsrfToken 值放在名為“_csrf”的請(qǐng)求參數(shù)或名為“X-CSRF-TOKEN”的請(qǐng)求頭字段里(可以調(diào)用相應(yīng)的設(shè)置方法來重新設(shè)定)羡洛。校驗(yàn)時(shí)挂脑,通過對(duì)比HttpSession內(nèi)存儲(chǔ)的CsrfToken值與前端攜帶的CsrfToken值是否一致,便能斷定本次請(qǐng)求是否為CSRF攻擊欲侮。

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

這種方式靈活性不足
Spring Security還提供了另一種方式崭闲,即CookieCsrfTokenRepository
CookieCsrfTokenRepository 是一種更加靈活可行的方案,它將 CsrfToken 值存儲(chǔ)在用戶的cookie內(nèi)威蕉。減少了服務(wù)器HttpSession存儲(chǔ)的內(nèi)存消耗刁俭,并且當(dāng)用cookie存儲(chǔ)CsrfToken值時(shí),前端可以用JavaScript讀热驼恰(需要設(shè)置該cookie的httpOnly屬性為false)牍戚,而不需要服務(wù)器注入?yún)?shù),在使用方式上更加靈活虑粥。

存儲(chǔ)在cookie中是不可以被CSRF利用的如孝,cookie 只有在同域的情況下才能被讀取,所以杜絕了第三方站點(diǎn)跨域獲取 CsrfToken 值的可能娩贷。CSRF攻擊本身是不知道cookie內(nèi)容的第晰,只是利用了當(dāng)請(qǐng)求自動(dòng)攜帶cookie時(shí)可以通過身份驗(yàn)證的漏洞。但服務(wù)器對(duì) CsrfToken 值的校驗(yàn)并非取自 cookie育勺,而是需要前端手動(dòng)將CsrfToken值作為參數(shù)攜帶在請(qǐng)求里

下面是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;
                //加載不到,則證明請(qǐng)求是首次發(fā)起的涧至,應(yīng)該生成并保存一個(gè)新的 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);

                //排除部分不需要驗(yàn)證CSRF攻擊的請(qǐng)求方法(默認(rèn)忽略了GET腹躁、HEAD、TRACE和OPTIONS)
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            filterChain.doFilter(request, response);
            return;
        }

                //實(shí)際的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);
    }

我們?cè)谌粘J褂弥心吓睿梢圆捎胔eader或者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>驗(yàn)證碼</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>

注意事項(xiàng)

springSecurity配置了默認(rèn)放行, 不需要通過csrfFilter過濾器檢測(cè)的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());
        }
    }

之所以會(huì)有上面默認(rèn)的GET,HEAD,TRACE,OPTIONS方式赘方,是因?yàn)?/p>

  1. 如果這個(gè)http請(qǐng)求是通過get方式發(fā)起的請(qǐng)求烧颖,意味著它只是訪問服務(wù)器 的資源,僅僅只是查詢窄陡,沒有更新服務(wù)器的資源炕淮,所以對(duì)于這類請(qǐng)求,spring security的防御策略是允許的跳夭;

  2. 如果這個(gè)http請(qǐng)求是通過post請(qǐng)求發(fā)起的涂圆, 那么spring security是默認(rèn)攔截這類請(qǐng)求的
    因?yàn)檫@類請(qǐng)求是帶有更新服務(wù)器資源的危險(xiǎn)操作们镜,如果惡意第三方可以通過劫持session id來更新 服務(wù)器資源,那會(huì)造成服務(wù)器數(shù)據(jù)被非法的篡改润歉,所以這類請(qǐng)求是會(huì)被Spring security攔截的模狭,在默認(rèn)的情況下,spring security是啟用csrf 攔截功能的踩衩,這會(huì)造成嚼鹉,在跨域的情況下,post方式提交的請(qǐng)求都會(huì)被攔截?zé)o法被處理(包括合理的post請(qǐng)求)驱富,前端發(fā)起的post請(qǐng)求后端無法正常 處理锚赤,雖然保證了跨域的安全性,但影響了正常的使用萌朱,如果關(guān)閉csrf防護(hù)功能宴树,雖然可以正常處理post請(qǐng)求,但是無法防范通過劫持session id的非法的post請(qǐng)求晶疼,所以spring security為了正確的區(qū)別合法的post請(qǐng)求酒贬,采用了token的機(jī)制 。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末翠霍,一起剝皮案震驚了整個(gè)濱河市锭吨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌寒匙,老刑警劉巖零如,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異锄弱,居然都是意外死亡考蕾,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門会宪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肖卧,“玉大人,你說我怎么就攤上這事掸鹅∪剩” “怎么了?”我有些...
    開封第一講書人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵巍沙,是天一觀的道長(zhǎng)葵姥。 經(jīng)常有香客問我,道長(zhǎng)句携,這世上最難降的妖魔是什么榔幸? 我笑而不...
    開封第一講書人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上削咆,老公的妹妹穿的比我還像新娘喳篇。我一直安慰自己,他們只是感情好态辛,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著挺尿,像睡著了一般奏黑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上编矾,一...
    開封第一講書人閱讀 51,598評(píng)論 1 305
  • 那天熟史,我揣著相機(jī)與錄音,去河邊找鬼窄俏。 笑死蹂匹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的凹蜈。 我是一名探鬼主播限寞,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼仰坦!你這毒婦竟也來了履植?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤悄晃,失蹤者是張志新(化名)和其女友劉穎玫霎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體妈橄,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡庶近,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了眷蚓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鼻种。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖溪椎,靈堂內(nèi)的尸體忽然破棺而出普舆,到底是詐尸還是另有隱情,我是刑警寧澤校读,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布沼侣,位于F島的核電站,受9級(jí)特大地震影響歉秫,放射性物質(zhì)發(fā)生泄漏蛾洛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望轧膘。 院中可真熱鬧钞螟,春花似錦、人聲如沸谎碍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蟆淀。三九已至拯啦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間熔任,已是汗流浹背褒链。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留疑苔,地道東北人甫匹。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像惦费,于是被迫代替她去往敵國(guó)和親兵迅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355