SpringSecurity跨域請(qǐng)求偽造(CSRF)的防護(hù)實(shí)現(xiàn)
一、CSRF
CSRF的全稱(chēng)是(Cross Site Request Forgery),可譯為跨域請(qǐng)求偽造,是一種利用用戶(hù)帶登錄 態(tài)的cookie進(jìn)行安全操作的攻擊方式。CSRF實(shí)際上并不難防,但常常被系統(tǒng)開(kāi)發(fā)者忽略,從而埋下巨 大的安全隱患。
二、攻擊過(guò)程
舉個(gè)例子,假設(shè)你登錄了郵箱,正常情況下可以通過(guò)某個(gè)鏈接http:xx.mail.com/send可以發(fā)送郵件。此時(shí)你又訪(fǎng)問(wèn)了別的網(wǎng)站,網(wǎng)站中有黃色廣告,點(diǎn)擊后廣告會(huì)請(qǐng)求http:xx.mail.com/send。此時(shí)相當(dāng)于在盜版網(wǎng)站中調(diào)用了發(fā)送郵件的鏈接,訪(fǎng)問(wèn)時(shí)會(huì)使用你郵箱網(wǎng)站的cookie信息。雖然盜版網(wǎng)站會(huì)提示跨域,但服務(wù)端任然進(jìn)行了相應(yīng)處理。
三、防御手段
在任何情況下,都應(yīng)當(dāng)盡可能地避免以GET方式提供涉及數(shù)據(jù)修改的API。并不是說(shuō)其他請(qǐng)求方式可以避免CSRF,只是GET請(qǐng)求更容易被攻擊。
在此基礎(chǔ)上,防御 CSRF攻擊的方式主要有以下兩種。
1.HTTP Referer
Http referer是由瀏覽器添加的一個(gè)請(qǐng)求頭字段,用于標(biāo)識(shí)請(qǐng)求來(lái)源,瀏覽器端無(wú)法輕易篡改該值。
比如攻擊者在第三方頁(yè)面構(gòu)造了POST請(qǐng)求,htttp referer不是我們網(wǎng)站的地址(有的老版IE瀏覽器可以修改該值,如果用戶(hù)的瀏覽器比較新,就能避免這個(gè)問(wèn)題),當(dāng)服務(wù)端收到請(qǐng)求,發(fā)現(xiàn)請(qǐng)求來(lái)自其他站點(diǎn),就能拒絕該請(qǐng)求。
這種方式簡(jiǎn)單便捷,但不是完全可靠,比如老的瀏覽器就能修改該值。用戶(hù)在瀏覽器設(shè)置了不被跟蹤,就不會(huì)有該字段,服務(wù)端加了校驗(yàn)后就會(huì)攔截掉用戶(hù)的正常請(qǐng)求。
2.CsrfToken認(rèn)證
CSRF是利用用戶(hù)的登錄態(tài)進(jìn)行攻擊的,而用戶(hù)的登錄態(tài)記錄在cookie中。其實(shí)攻擊者并不知道用 戶(hù)的cookie存放了哪些數(shù)據(jù),于是想方設(shè)法讓用戶(hù)自身發(fā)起請(qǐng)求,這樣瀏覽器便會(huì)自行將cookie傳送到 服務(wù)器完成身份校驗(yàn)。
CsrfToken 的防范思路是,添加一些并不存放于 cookie 的驗(yàn)證值,并在每個(gè)請(qǐng)求中都進(jìn)行校驗(yàn), 便可以阻止CSRF攻擊。
具體做法是在用戶(hù)登錄時(shí),由系統(tǒng)發(fā)放一個(gè)CsrfToken值,用戶(hù)攜帶該CsrfToken值與用戶(hù)名、密碼 等參數(shù)完成登錄。服務(wù)端記錄該會(huì)話(huà)的CsrfToken值,之后在用戶(hù)的任何請(qǐng)求中,都必須帶上該 CsrfToken值,并由系統(tǒng)進(jìn)行校驗(yàn)。
該方案需要前端配合,包括存儲(chǔ)CsrfToken的值,在每次的請(qǐng)求中,不管是form表單還是ajax,都需要攜帶該token。雖然比HTTP Referer安全很多,但也有弊端,如果在已有系統(tǒng)進(jìn)行改造,就需要修改每一個(gè)請(qǐng)求,所以建議在系統(tǒng)開(kāi)發(fā)之初就考慮防御CSRF攻擊。
三、使用SpringSecurity防御CSRF
csrf攻擊完全是基于瀏覽器的,如果前端沒(méi)有瀏覽器,也就不會(huì)有CSRF攻擊了,所以我們需要關(guān)閉SpringSecurity自動(dòng)配置的csrf。
1.SpringSecurity防御CSRF過(guò)程
CsrfFilter:
SpringSecurity通過(guò)注冊(cè)一個(gè)CsrfFilter來(lái)專(zhuān)門(mén)處理CSRF攻擊。
CsrfToken:
用該接口來(lái)定義csrftoekn所需的一些必要方法。
public interface CsrfToken extends Serializable { //從哪個(gè)頭字段獲取token值 String getHeaderName(); //從哪個(gè)參數(shù)獲取token值 String getParameterName(); String getToken(); }
CsrfTokenRepository
定義了如何生成,保存、以及加載token.
public interface CsrfTokenRepository { CsrfToken generateToken(HttpServletRequest request); void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response); CsrfToken loadToken(HttpServletRequest request); }
HttpSessionCsrfTokenRepository
默認(rèn)情況下,SpringSecurity使用的CsrfTokenRepository的實(shí)現(xiàn)類(lèi)是HttpSessionCsrfTokenRepository
public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository { private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf"; private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN"; private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName() .concat(".CSRF_TOKEN"); private String parameterName = DEFAULT_CSRF_PARAMETER_NAME; private String headerName = DEFAULT_CSRF_HEADER_NAME; private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME; @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { if (token == null) { HttpSession session = request.getSession(false); if (session != null) { session.removeAttribute(this.sessionAttributeName); } } else { HttpSession session = request.getSession(); session.setAttribute(this.sessionAttributeName, token); } } @Override public CsrfToken loadToken(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return null; } return (CsrfToken) session.getAttribute(this.sessionAttributeName); } @Override public CsrfToken generateToken(HttpServletRequest request) { return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken()); } public void setParameterName(String parameterName) { Assert.hasLength(parameterName, "parameterName cannot be null or empty"); this.parameterName = parameterName; } public void setHeaderName(String headerName) { Assert.hasLength(headerName, "headerName cannot be null or empty"); this.headerName = headerName; } public void setSessionAttributeName(String sessionAttributeName) { Assert.hasLength(sessionAttributeName, "sessionAttributename cannot be null or empty"); this.sessionAttributeName = sessionAttributeName; } private String createNewToken() { return UUID.randomUUID().toString(); } }
HttpSessionCsrfTokenRepository將CsrfToken值存儲(chǔ)在HttpSession中,并指定前端把CsrfToken 值放在名為“_csrf”的請(qǐng)求參數(shù)或名為“X-CSRF-TOKEN”的請(qǐng)求頭字段里(可以調(diào)用相應(yīng)的設(shè)置方法來(lái)重新設(shè)定)。校驗(yàn)時(shí),通過(guò)對(duì)比HttpSession內(nèi)存儲(chǔ)的CsrfToken值與前端攜帶的CsrfToken值是否一致,便能斷定本次請(qǐng)求是否為CSRF攻擊。
前端使用Token的時(shí)候,必須使用從服務(wù)端渲染的方式,比如jsp頁(yè)面:
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
CookieCsrfTokenRepository
Spring Security還提供了另一種方式,即CookieCsrfTokenRepository。之前是服務(wù)端將token存儲(chǔ)在了session中。這個(gè)是將token存儲(chǔ)在瀏覽器的cookie中,這樣可以減少服務(wù)端的內(nèi)存消耗,而且前端可以使用js讀?。ㄐ枰O(shè)置該cookie的httpOnly屬性為false),更加靈活。
有人可能會(huì)有疑問(wèn),放在cookie中,不是又可以被攻擊了嗎?其實(shí)不是的。
cookie只有在同域的情況下才能被js獲取。正常情況下,服務(wù)端從cookie中獲取token,前端使用js從cookie中獲取token,2者進(jìn)行校驗(yàn)。攻擊者只能在第三方頁(yè)面?zhèn)卧煺?qǐng)求的時(shí)候,利用請(qǐng)求攜帶cookie,這個(gè)時(shí)候服務(wù)端能拿從攜帶的cookie中拿到token,但是前端并沒(méi)有使用js將用于校驗(yàn)的token傳給服務(wù)端(攻擊者沒(méi)法獲取cookie),所以校驗(yàn)沒(méi)法通過(guò)。
CsrfFilter
現(xiàn)在我們重新來(lái)看這個(gè)類(lèi)的主要邏輯:
@Override 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); if (!this.requireCsrfProtectionMatcher.matches(request)) { if (this.logger.isTraceEnabled()) { this.logger.trace("Did not protect against CSRF since request did not match " + this.requireCsrfProtectionMatcher); } filterChain.doFilter(request, response); return; } String actualToken = request.getHeader(csrfToken.getHeaderName()); if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } if (!equalsConstantTime(csrfToken.getToken(), actualToken)) { this.logger.debug( LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request))); AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken) : new MissingCsrfTokenException(actualToken); this.accessDeniedHandler.handle(request, response, exception); return; } filterChain.doFilter(request, response); }
這段代碼的意思就是, 從你指定或者默認(rèn)的的CsrfTokenRepository中獲取token,其實(shí)就是獲取的服務(wù)端存儲(chǔ)的token(session中或者cookie中),如果沒(méi)有,那么就生成并且保存token,然后獲取前端傳過(guò)來(lái)的token,然后進(jìn)行對(duì)比。
2.SpringSecurity配置CSRF
1.我們使用cookie的方式存儲(chǔ)token.
2.添加AccessDeniedHandler
用來(lái)在token請(qǐng)求不通過(guò)的時(shí)候,返回?cái)?shù)據(jù)。
@Component @Slf4j public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JSON.toJSONString(ResultVO.error(10000, "禁止訪(fǎng)問(wèn)"))); } }
3. 前端修改
生成的token:
function getCookie(name){ var strcookie = document.cookie;//獲取cookie字符串 var arrcookie = strcookie.split("; ");//分割 //遍歷匹配 for ( var i = 0; i < arrcookie.length; i++) { var arr = arrcookie[i].split("="); if (arr[0] === name){ return arr[1]; } } return ""; }
3.啟動(dòng)項(xiàng)目測(cè)試
啟動(dòng)項(xiàng)目,登錄成功,跳轉(zhuǎn)頁(yè)面。
文章配套代碼:https://gitee.com/lookoutthebush/spring-security-demo
到此這篇關(guān)于SpringSecurity跨域請(qǐng)求偽造(CSRF)的防護(hù)實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SpringSecurity CSRF防護(hù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Linux部署springboot項(xiàng)目彩色日志打印方式
這篇文章主要介紹了Linux部署springboot項(xiàng)目彩色日志打印方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04Java對(duì)象轉(zhuǎn)json JsonFormat注解
這篇文章主要介紹了Java對(duì)象轉(zhuǎn)json JsonFormat注解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05spring cloud gateway中如何讀取請(qǐng)求參數(shù)
這篇文章主要介紹了spring cloud gateway中如何讀取請(qǐng)求參數(shù)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07spring*.xml配置文件明文加密的實(shí)現(xiàn)
這篇文章主要介紹了spring*.xml配置文件明文加密的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01java定時(shí)任務(wù)cron表達(dá)式每周執(zhí)行一次的坑及解決
這篇文章主要介紹了java定時(shí)任務(wù)cron表達(dá)式每周執(zhí)行一次的坑及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06Java8中List轉(zhuǎn)換String字符串幾種方式
這篇文章主要給大家介紹了關(guān)于Java8中List轉(zhuǎn)換String字符串的幾種方式,在實(shí)際開(kāi)發(fā)中經(jīng)常遇到List轉(zhuǎn)為String字符串的情況,文中給出了幾種方法的示例代碼,需要的朋友可以參考下2023-07-07IntelliJ idea 如何生成動(dòng)態(tài)的JSON字符串(步驟詳解)
這篇文章主要介紹了IntelliJ idea 如何生成動(dòng)態(tài)的JSON字符串,本文分步驟給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08

談?wù)凧ava中對(duì)象,類(lèi)和this,super,static關(guān)鍵字的使用