AdSense

網頁

2020/4/10

Spring Security CSRF防護 CsrfFilter原始碼解析

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_MATCHERCsrfFilter的內部類別DefaultRequiresCsrfMatcher的實例。在DefaultRequiresCsrfMatcher的建構式中定義了預設允許通過CSRF防護的HTTP Method,分別為GETHEADTRACEOPTIONSHTTP Safe Methods),也就是說這些以外的HTTP Method如POSTPUTDELETEPATCH等才需要進行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幾乎要把原始碼翻一遍才會使用。


參考:

沒有留言:

AdSense