Spring Security (5.2.1.RELEASE) CSRF防護驗證原始碼解析。
Spring Security CSRF防護的邏輯處理主要是落在CsrfFilter
類別中。節錄CsrfFilter
原始碼如下。
CsrfFilter
package org.springframework.security.web.csrf;
import //...
public final class CsrfFilter extends OncePerRequestFilter {
public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfFilter.DefaultRequiresCsrfMatcher();
// ...
private final CsrfTokenRepository tokenRepository;
private RequestMatcher requireCsrfProtectionMatcher;
private AccessDeniedHandler accessDeniedHandler;
public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER; // 即內部類別CsrfFilter.DefaultRequiresCsrfMatcher()
this.accessDeniedHandler = new AccessDeniedHandlerImpl(); // 預設的CSRF防護驗證失敗處理器
Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
this.tokenRepository = csrfTokenRepository; // HttpSessionCsrfTokenRepository
}
// ...
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request); // 實際是從HttpSession取得CsrfToken
boolean missingToken = csrfToken == null;
if (missingToken) { // 如果HttpSession不存在CsrfToken則產生一個新的並存在Session中
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken); // 把CsrfToken存在request attribute,key為"CsrfToken"
request.setAttribute(csrfToken.getParameterName(), csrfToken); // 把CsrfToken存在request attribute,key為"_csrf"
if (!this.requireCsrfProtectionMatcher.matches(request)) { // 預設GET,HEAD,TRACE,OPTIONS不做CSRF防護驗證
filterChain.doFilter(request, response);
} else {
String actualToken = request.getHeader(csrfToken.getHeaderName()); // 以"X-CSRF-TOKEN"取得請求實際送來的token
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));
}
// HttpSession中沒有token或傳入的token與HttpSession現存的token不匹配,丟出403 Forbidden錯誤
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); // 通過CSRF防護驗證後,繼續下一個filter安全驗證。
}
}
}
// ...
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
private final HashSet<String> allowedMethods;
private DefaultRequiresCsrfMatcher() {
this.allowedMethods = new HashSet(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
}
public boolean matches(HttpServletRequest request) {
return !this.allowedMethods.contains(request.getMethod());
}
}
}
成員RequestMatcher DEFAULT_CSRF_MATCHER
為CsrfFilter
的內部類別DefaultRequiresCsrfMatcher
的實例。在DefaultRequiresCsrfMatcher
的建構式中定義了預設允許通過CSRF防護的HTTP Method,分別為GET
,HEAD
,TRACE
,OPTIONS
(HTTP Safe Methods),也就是說這些以外的HTTP Method如POST
,PUT
,DELETE
,PATCH
等才需要進行CSRF安全驗證。
若有開啟Spring Security CSRF保護,當請求資源時需通過CsrfFilter.doFilterInternal()
的檢驗。檢驗時比對既有的CsrfToken
(DefaultCsrfToken
)對象預設是透過HttpSessionCsrfTokenRepository.loadToken()
從請求中的HttpSession
取得 ;若找不到CsrfToken
會透過AccessDeniedHandlerImpl.handle()
返回403 Forbidden (HttpStatus.FORBIDDEN
)錯誤。
隨請求送來實際的CSRF Token字串是夾帶在Request Headers,key預設是X-CSRF-TOKEN
,定義在HttpSessionCsrfTokenRepository
的成員headerName = "X-CSRF-TOKEN"
。若傳來的token與HttpSession
中儲存的token不一致會也是透過AccessDeniedHandlerImpl.handle()
返回403 Forbidden錯誤。
所以若是GET
請求不需要提供CSRF Token,因為Spring Security CSRF防護預設不保護GET
。
若是POST
請求則應在Reqeust Headers加入key為X-CSRF-TOKEN
的CSRF Token UUID字串,例如055199e8-12e2-47e7-9ea7-a2ae15b7dd25
。
總覺得每次碰到Spring Secuirty幾乎要把原始碼翻一遍才會使用。
參考:
沒有留言:
張貼留言