SpringSecurity跨域請求偽造(CSRF)的防護(hù)實現(xiàn)
一、CSRF
CSRF的全稱是(Cross Site Request Forgery),可譯為跨域請求偽造,是一種利用用戶帶登錄 態(tài)的cookie進(jìn)行安全操作的攻擊方式。CSRF實際上并不難防,但常常被系統(tǒng)開發(fā)者忽略,從而埋下巨 大的安全隱患。
二、攻擊過程
舉個例子,假設(shè)你登錄了郵箱,正常情況下可以通過某個鏈接http:xx.mail.com/send可以發(fā)送郵件。此時你又訪問了別的網(wǎng)站,網(wǎng)站中有黃色廣告,點擊后廣告會請求http:xx.mail.com/send。此時相當(dāng)于在盜版網(wǎng)站中調(diào)用了發(fā)送郵件的鏈接,訪問時會使用你郵箱網(wǎng)站的cookie信息。雖然盜版網(wǎng)站會提示跨域,但服務(wù)端任然進(jìn)行了相應(yīng)處理。
三、防御手段
在任何情況下,都應(yīng)當(dāng)盡可能地避免以GET方式提供涉及數(shù)據(jù)修改的API。并不是說其他請求方式可以避免CSRF,只是GET請求更容易被攻擊。
在此基礎(chǔ)上,防御 CSRF攻擊的方式主要有以下兩種。
1.HTTP Referer
Http referer是由瀏覽器添加的一個請求頭字段,用于標(biāo)識請求來源,瀏覽器端無法輕易篡改該值。
比如攻擊者在第三方頁面構(gòu)造了POST請求,htttp referer不是我們網(wǎng)站的地址(有的老版IE瀏覽器可以修改該值,如果用戶的瀏覽器比較新,就能避免這個問題),當(dāng)服務(wù)端收到請求,發(fā)現(xiàn)請求來自其他站點,就能拒絕該請求。
這種方式簡單便捷,但不是完全可靠,比如老的瀏覽器就能修改該值。用戶在瀏覽器設(shè)置了不被跟蹤,就不會有該字段,服務(wù)端加了校驗后就會攔截掉用戶的正常請求。
2.CsrfToken認(rèn)證
CSRF是利用用戶的登錄態(tài)進(jìn)行攻擊的,而用戶的登錄態(tài)記錄在cookie中。其實攻擊者并不知道用 戶的cookie存放了哪些數(shù)據(jù),于是想方設(shè)法讓用戶自身發(fā)起請求,這樣瀏覽器便會自行將cookie傳送到 服務(wù)器完成身份校驗。
CsrfToken 的防范思路是,添加一些并不存放于 cookie 的驗證值,并在每個請求中都進(jìn)行校驗, 便可以阻止CSRF攻擊。
具體做法是在用戶登錄時,由系統(tǒng)發(fā)放一個CsrfToken值,用戶攜帶該CsrfToken值與用戶名、密碼 等參數(shù)完成登錄。服務(wù)端記錄該會話的CsrfToken值,之后在用戶的任何請求中,都必須帶上該 CsrfToken值,并由系統(tǒng)進(jìn)行校驗。
該方案需要前端配合,包括存儲CsrfToken的值,在每次的請求中,不管是form表單還是ajax,都需要攜帶該token。雖然比HTTP Referer安全很多,但也有弊端,如果在已有系統(tǒng)進(jìn)行改造,就需要修改每一個請求,所以建議在系統(tǒng)開發(fā)之初就考慮防御CSRF攻擊。
三、使用SpringSecurity防御CSRF
csrf攻擊完全是基于瀏覽器的,如果前端沒有瀏覽器,也就不會有CSRF攻擊了,所以我們需要關(guān)閉SpringSecurity自動配置的csrf。

1.SpringSecurity防御CSRF過程
CsrfFilter:
SpringSecurity通過注冊一個CsrfFilter來專門處理CSRF攻擊。
CsrfToken:
用該接口來定義csrftoekn所需的一些必要方法。
public interface CsrfToken extends Serializable {
//從哪個頭字段獲取token值
String getHeaderName();
//從哪個參數(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的實現(xiàn)類是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值存儲在HttpSession中,并指定前端把CsrfToken 值放在名為“_csrf”的請求參數(shù)或名為“X-CSRF-TOKEN”的請求頭字段里(可以調(diào)用相應(yīng)的設(shè)置方法來重新設(shè)定)。校驗時,通過對比HttpSession內(nèi)存儲的CsrfToken值與前端攜帶的CsrfToken值是否一致,便能斷定本次請求是否為CSRF攻擊。
前端使用Token的時候,必須使用從服務(wù)端渲染的方式,比如jsp頁面:
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>CookieCsrfTokenRepository
Spring Security還提供了另一種方式,即CookieCsrfTokenRepository。之前是服務(wù)端將token存儲在了session中。這個是將token存儲在瀏覽器的cookie中,這樣可以減少服務(wù)端的內(nèi)存消耗,而且前端可以使用js讀取(需要設(shè)置該cookie的httpOnly屬性為false),更加靈活。
有人可能會有疑問,放在cookie中,不是又可以被攻擊了嗎?其實不是的。
cookie只有在同域的情況下才能被js獲取。正常情況下,服務(wù)端從cookie中獲取token,前端使用js從cookie中獲取token,2者進(jìn)行校驗。攻擊者只能在第三方頁面?zhèn)卧煺埱蟮臅r候,利用請求攜帶cookie,這個時候服務(wù)端能拿從攜帶的cookie中拿到token,但是前端并沒有使用js將用于校驗的token傳給服務(wù)端(攻擊者沒法獲取cookie),所以校驗沒法通過。
CsrfFilter
現(xiàn)在我們重新來看這個類的主要邏輯:
@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,其實就是獲取的服務(wù)端存儲的token(session中或者cookie中),如果沒有,那么就生成并且保存token,然后獲取前端傳過來的token,然后進(jìn)行對比。
2.SpringSecurity配置CSRF
1.我們使用cookie的方式存儲token.

2.添加AccessDeniedHandler
用來在token請求不通過的時候,返回數(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, "禁止訪問")));
}
}
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.啟動項目測試
啟動項目,登錄成功,跳轉(zhuǎn)頁面。
文章配套代碼:https://gitee.com/lookoutthebush/spring-security-demo
到此這篇關(guān)于SpringSecurity跨域請求偽造(CSRF)的防護(hù)實現(xiàn)的文章就介紹到這了,更多相關(guān)SpringSecurity CSRF防護(hù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java 運(yùn)算符 動力節(jié)點Java學(xué)院整理
這篇文章主要介紹了Java 運(yùn)算符 動力節(jié)點Java學(xué)院整理,需要的朋友可以參考下2017-04-04
Springboot多數(shù)據(jù)源配置之整合dynamic-datasource方式
這篇文章主要介紹了Springboot多數(shù)據(jù)源配置之整合dynamic-datasource方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03
Maven項目部署到Jboss出現(xiàn)Failed to create a new SAX parser
這篇文章主要為大家詳細(xì)介紹了Maven項目部署到Jboss出現(xiàn)Failed to create a new SAX parser的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11
Mybatis-plus中的@EnumValue注解使用詳解
這篇文章主要介紹了Mybatis-plus中的@EnumValue注解使用詳解,在PO類中,如果我們直接使用枚舉類型去映射數(shù)據(jù)庫的對應(yīng)字段保存時,往往就會因為類型不匹配導(dǎo)致映射失敗,Mybatis-plus提供了一種解決辦法,就是使用@EnumValue注解,需要的朋友可以參考下2024-02-02
springboot tomcat最大線程數(shù)與最大連接數(shù)解析
這篇文章主要介紹了springboot tomcat最大線程數(shù)與最大連接數(shù)解析,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06

