SpringBoot從入門到精通系列(專欄導航)
964
2022-05-30
SpringSecurity核心過濾器-CsrfFilter
Spring Security除了認證授權外功能外,還提供了安全防護功能。本文我們來介紹下SpringSecurity中是如何阻止CSRF攻擊的。
一、什么是CSRF攻擊
跨站請求偽造(英語:Cross-site request forgery),也被稱為 one-click attack 或者 session riding,通常縮寫為 CSRF 或者 XSRF, 是一種挾制用戶在當前已登錄的 Web 應用程序上執行非本意的操作的攻擊方法。跟跨網站腳本(XSS)相比,XSS利用的是用戶對指定網站的信任,CSRF 利用的是網站對用戶網頁瀏覽器的信任。
跨站請求攻擊,簡單地說,是攻擊者通過一些技術手段欺騙用戶的瀏覽器去訪問一個自己曾經認證過的網站并運行一些操作(如發郵件,發消息,甚至財產操作如轉賬和購買商品)。由于瀏覽器曾經認證過,所以被訪問的網站會認為是真正的用戶操作而去運行。這利用了 web 中用戶身份驗證的一個漏洞:簡單的身份驗證只能保證請求發自某個用戶的瀏覽器,卻不能保證請求本身是用戶自愿發出的。舉個例子如下:
二、解決方案
1.檢查Referer字段
HTTP頭中有一個Referer字段,這個字段用以標明請求來源于哪個地址。在處理敏感數據請求時,通常來說,Referer字段應和請求的地址位于同一域名下。以上文銀行操作為例,Referer字段地址通常應該是轉賬按鈕所在的網頁地址,應該也位于www.bankchina.com之下。而如果是CSRF攻擊傳來的請求,Referer字段會是包含惡意網址的地址,不會位于www.bankhacker.com之下,這時候服務器就能識別出惡意的訪問。
這種辦法簡單易行,工作量低,僅需要在關鍵訪問處增加一步校驗。但這種辦法也有其局限性,因其完全依賴瀏覽器發送正確的Referer字段。雖然http協議對此字段的內容有明確的規定,但并無法保證來訪的瀏覽器的具體實現,亦無法保證瀏覽器沒有安全漏洞影響到此字段。并且也存在攻擊者攻擊某些瀏覽器,篡改其Referer字段的可能。
2.CsrfToken
其實CSRF攻擊是在用戶登錄且沒有退出瀏覽器的情況下訪問了第三方的站點而被攻擊的,完全是攜帶了認證的cookie來實現的,我們只需要在服務端響應給客戶端的頁面中綁定隨機的信息,然后提交請求后在服務端校驗,如果攜帶的數據和之前的不一致就認為是CSRF攻擊,拒絕這些請求即可。
三、SpringSecurity是如何防止CSRF攻擊的
首先從 Spring Security 4.0 開始,默認情況下會啟用 CSRF 保護,以防止 CSRF 攻擊應用程序,Spring Security CSRF 會針對 PATCH,POST,PUT 和 DELETE 方法進行防護。
1.開啟關閉CSRF防御
在SpringSecurity中默認是開啟csrf防御的,我們可以通過一下配置來關閉csrf防御
http.csrf().disable();
1
或者在基于配置文件的使用中使用如下操作關閉
1
2.SpringSecurity的實現
2.1 CSRF的原理
生成csrfToken保存到HttpSession或者Cookie中
請求到來時,程序會從請求中獲取提交的csrfToken,同時會從HttpSession中獲取之前存儲的csrfToken進行比較,如果相同則認為是合法的請求,繼續后面的操作,如果不相等則認為是CSRF工具,拒絕該請求
2.2 源碼分析
然后我們來看下SpringSecurity中的代碼是如何實現的。我們主要看的是 spring-security-web.jar中的
org.springframework.security.web.csrf包下的源碼。
CsrfToken是一個非常簡單的接口,定義了Token令牌,消息頭和請求參數。
public interface CsrfToken extends Serializable { /** * 獲取我們放置在請求頭中CSRF隨機值的名稱 */ String getHeaderName(); /** * 獲取請求體中的csrf隨機值的參數名稱 */ String getParameterName(); /** * 返回具體的Token值 */ String getToken(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CsrfToken的默認實現是DefaultCsrfToken。
CsrfTokenRepository接口也非常簡單,定義了Token的生成,存儲和獲取的相關API
public interface CsrfTokenRepository { /** * 生成Token */ CsrfToken generateToken(HttpServletRequest request); /** * 存儲生成的Token */ void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response); /** * 返回Token */ CsrfToken loadToken(HttpServletRequest request); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CsrfTokenRepository的實現在SpringSecurity中有兩個實現。
默認的實現是HttpSessionCsrfTokenRepository。是一個基于HttpSession保存csrfToken的實現。
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; // 保存Token到session中 @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); } } // 從session中加載token @Override public CsrfToken loadToken(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return null; } return (CsrfToken) session.getAttribute(this.sessionAttributeName); } // 生成Token @Override public CsrfToken generateToken(HttpServletRequest request) { return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken()); } /** * Sets the {@link HttpServletRequest} parameter name that the {@link CsrfToken} is * expected to appear on * @param parameterName the new parameter name to use */ public void setParameterName(String parameterName) { Assert.hasLength(parameterName, "parameterName cannot be null or empty"); this.parameterName = parameterName; } /** * Sets the header name that the {@link CsrfToken} is expected to appear on and the * header that the response will contain the {@link CsrfToken}. * @param headerName the new header name to use */ public void setHeaderName(String headerName) { Assert.hasLength(headerName, "headerName cannot be null or empty"); this.headerName = headerName; } /** * Sets the {@link HttpSession} attribute name that the {@link CsrfToken} is stored in * @param sessionAttributeName the new attribute name to use */ public void setSessionAttributeName(String sessionAttributeName) { Assert.hasLength(sessionAttributeName, "sessionAttributename cannot be null or empty"); this.sessionAttributeName = sessionAttributeName; } // 通過UUID來生成Token信息 private String createNewToken() { return UUID.randomUUID().toString(); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
CsrfFilter用于處理跨站請求偽造。檢查表單提交的_csrf隱藏域的value與內存中保存的的是否一致,如果一致框架則認為當然登錄頁面是安全的,如果不一致,會報403forbidden錯誤。
具體處理請求的方法
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(HttpServletResponse.class.getName(), response); // 從session中加載 Token CsrfToken csrfToken = this.tokenRepository.loadToken(request); boolean missingToken = (csrfToken == null); // 如果是第一次訪問就生成Token信息 if (missingToken) { csrfToken = this.tokenRepository.generateToken(request); // 把生成的Token信息存儲在Session中 this.tokenRepository.saveToken(csrfToken, request, response); } request.setAttribute(CsrfToken.class.getName(), csrfToken); request.setAttribute(csrfToken.getParameterName(), csrfToken); // 匹配是否是需要做CSRF防御的相關請求 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; } // 獲取請求攜帶在header中的Token信息 String actualToken = request.getHeader(csrfToken.getHeaderName()); if (actualToken == null) { // 從請求參數中獲取Token信息 actualToken = request.getParameter(csrfToken.getParameterName()); } // 判斷請求中的Token是否和Session中存儲的Token相等 if (!equalsConstantTime(csrfToken.getToken(), actualToken)) { this.logger.debug( LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request))); // Token不相等,說明是CSRF攻擊,拋出訪問拒絕的異常 AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken) : new MissingCsrfTokenException(actualToken); this.accessDeniedHandler.handle(request, response, exception); return; } // 說明是正常的訪問,放過 filterChain.doFilter(request, response); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
3.分布式Session
上面介紹的CsrfToken校驗,生成的Token信息是存儲在HttpSession中的,那么我們在分布式環境下,跨進程的場景下我們要如何實現Session共享呢?這時我們可以通過SpringSession來實現,但是這里有個前提就是分布式的項目必須都得是在一個一級域名下的多個二級域名是可以實現的。
3.1 配置SpringSession
配置SpringSession可以參考Spring的官網:https://docs.spring.io/spring-session/docs/2.5.6/reference/html5/ 因為在分布式Session我們需要把Session數據獨立的存儲在Redis服務中,所以還需要啟動Redis服務。
添加相關依賴:
1
2
3
4
5
6
7
8
然后添加對應的配置
spring.redis.host=192.168.56.100 spring.redis.port=6379 spring.session.store-type=redis spring.session.redis.namespace=spring:session
1
2
3
4
修改host文件,設置域名關系
添加配置文件,設置Cookie中的domain為一級域名
@Configuration public class MySessionConfig { @Bean public CookieSerializer cookieSerializer(){ DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer(); cookieSerializer.setDomainName("msb.com"); cookieSerializer.setCookieName("csrfSession"); return cookieSerializer; } }
1
2
3
4
5
6
7
8
9
10
11
然后測試看效果,然后aa.msb.com:8080
然后訪問bb.msb.com:8081
可以看到兩個頁面中生成的csrfToken是一樣的,說明共享了數據,而且Cookie中的Session信息也一致。
搞定~
域名注冊服務 網站
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。