CSRF(Cross-site request forgery)跨站請(qǐng)求偽造,也被稱為One Click Attack或者Session Riding,通??s寫為CSRF或XSRF,是一種對(duì)網(wǎng)站的惡意利用。盡管聽起來像跨站腳本(XSS),但它與XSS非常不同,XSS利用站點(diǎn)內(nèi)的信任用戶,而CSRF則通過偽裝成受信任用戶的請(qǐng)求來利用受信任的網(wǎng)站。與XSS攻擊相比,CSRF攻擊往往不大流行(因此對(duì)其進(jìn)行防范的資源也相當(dāng)稀少)和難以防范,所以被認(rèn)為比XSS更具危險(xiǎn)性。
成都創(chuàng)新互聯(lián)公司專注于大豐網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗(yàn)。 熱誠(chéng)為您提供大豐營(yíng)銷型網(wǎng)站建設(shè),大豐網(wǎng)站制作、大豐網(wǎng)頁(yè)設(shè)計(jì)、大豐網(wǎng)站官網(wǎng)定制、微信小程序服務(wù),打造大豐網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供大豐網(wǎng)站排名全網(wǎng)營(yíng)銷落地服務(wù)。
CSRF是一種依賴web瀏覽器的、被混淆過的代理人攻擊(deputy attack)。
如何防御
使用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)求
public final class CsrfFilter extends OncePerRequestFilter { public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfFilter.DefaultRequiresCsrfMatcher(); private final Log logger = LogFactory.getLog(this.getClass()); private final CsrfTokenRepository tokenRepository; private RequestMatcher requireCsrfProtectionMatcher; private AccessDeniedHandler accessDeniedHandler; public CsrfFilter(CsrfTokenRepository csrfTokenRepository) { this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER; this.accessDeniedHandler = new AccessDeniedHandlerImpl(); Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null"); this.tokenRepository = csrfTokenRepository; } //通過這里可以看出SpringSecurity的csrf機(jī)制把請(qǐng)求方式分成兩類來處理 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(HttpServletResponse.class.getName(), response); CsrfToken csrfToken = this.tokenRepository.loadToken(request); boolean missingToken = csrfToken == null; if (missingToken) { csrfToken = this.tokenRepository.generateToken(request); this.tokenRepository.saveToken(csrfToken, request, response); } request.setAttribute(CsrfToken.class.getName(), csrfToken); request.setAttribute(csrfToken.getParameterName(), csrfToken); //第一類:"GET", "HEAD", "TRACE", "OPTIONS"四類請(qǐng)求可以直接通過 if (!this.requireCsrfProtectionMatcher.matches(request)) { filterChain.doFilter(request, response); } else { //第二類:除去上面四類,包括POST都要被驗(yàn)證攜帶token才能通過 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)); } } else { filterChain.doFilter(request, response); } } } public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) { Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null"); this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher; } public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) { Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null"); this.accessDeniedHandler = accessDeniedHandler; } private static final class DefaultRequiresCsrfMatcher implements RequestMatcher { private final HashSetallowedMethods; private DefaultRequiresCsrfMatcher() { this.allowedMethods = new HashSet(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS")); } public boolean matches(HttpServletRequest request) { return !this.allowedMethods.contains(request.getMethod()); } } }
禁用Csrf
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http //關(guān)閉打開的csrf保護(hù) .csrf().disable(); } }
用戶登錄時(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)求頭字段的接口
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); }
HttpSessionCsrfTokenRepository
在默認(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攻擊。
這種方式在某些單頁(yè)應(yīng)用中局限性比較大,靈活性不足。
CookieCsrfTokenRepository
Spring Security還提供了另一種方式,即CookieCsrfTokenRepository
CookieCsrfTokenRepository 是一種更加靈活可行的方案,它將 CsrfToken 值存儲(chǔ)在用戶的cookie內(nèi)。減少了服務(wù)器HttpSession存儲(chǔ)的內(nèi)存消耗,并且當(dāng)用cookie存儲(chǔ)CsrfToken值時(shí),前端可以用JavaScript讀?。ㄐ枰O(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中獲?。℉ttpSessionCsrfTokenRepository) 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); }
用戶想要堅(jiān)持CSRF Token在cookie中。 默認(rèn)情況下CookieCsrfTokenRepository將編寫一個(gè)名為 XSRF-TOKEN的cookie和從頭部命名 X-XSRF-TOKEN中讀取或HTTP參數(shù) _csrf。
//代碼如下: .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
我們?cè)谌粘J褂弥?,可以采用header或者param的方式添加csrf_token,下面示范從cookie中獲取token
注意事項(xiàng)
springSecurity配置了默認(rèn)放行, 不需要通過csrfFilter過濾器檢測(cè)的http訪問方式
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher { private final HashSetallowedMethods = 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>
如果這個(gè)http請(qǐng)求是通過get方式發(fā)起的請(qǐng)求,意味著它只是訪問服務(wù)器 的資源,僅僅只是查詢,沒有更新服務(wù)器的資源,所以對(duì)于這類請(qǐng)求,spring security的防御策略是允許的;
如果這個(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ī)制 。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。