上篇文章鬆哥和你們聊了什麼是 CSRF 攻擊,以及 CSRF 攻擊要如何防護。主要和你們聊了 Spring Security 中處理該問題的幾種辦法。html
今天鬆哥來和你們簡單的看一下 Spring Security 中,CSRF 防護源碼。前端
本文是本系列第 19 篇,閱讀本系列前面文章有助於更好的理解本文:java
本文主要從兩個方面來和你們講解:數據庫
_csrf
參數是如何生成的。_csrf
參數是如何校驗的。咱們先來看一下 Spring Security 中的 csrf 參數是如何生成的。後端
首先,Spring Security 中提供了一個保存 csrf 參數的規範,就是 CsrfToken:安全
public interface CsrfToken extends Serializable { String getHeaderName(); String getParameterName(); String getToken(); }
這裏三個方法都好理解,前兩個是獲取 _csrf
參數的 key,第三個是獲取 _csrf
參數的 value。cookie
CsrfToken 有兩個實現類,以下:session
默認狀況下使用的是 DefaultCsrfToken,咱們來稍微看下 DefaultCsrfToken:前後端分離
public final class DefaultCsrfToken implements CsrfToken { private final String token; private final String parameterName; private final String headerName; public DefaultCsrfToken(String headerName, String parameterName, String token) { this.headerName = headerName; this.parameterName = parameterName; this.token = token; } public String getHeaderName() { return this.headerName; } public String getParameterName() { return this.parameterName; } public String getToken() { return this.token; } }
這段實現很簡單,幾乎沒有添加額外的方法,就是接口方法的實現。dom
CsrfToken 至關於就是 _csrf
參數的載體。那麼參數是如何生成和保存的呢?這涉及到另一個類:
public interface CsrfTokenRepository { CsrfToken generateToken(HttpServletRequest request); void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response); CsrfToken loadToken(HttpServletRequest request); }
這裏三個方法:
CsrfTokenRepository 有四個實現類,在上篇文章中,咱們用到了其中兩個:HttpSessionCsrfTokenRepository 和 CookieCsrfTokenRepository,其中 HttpSessionCsrfTokenRepository 是默認的方案。
咱們先來看下 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; 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); } } public CsrfToken loadToken(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return null; } return (CsrfToken) session.getAttribute(this.sessionAttributeName); } public CsrfToken generateToken(HttpServletRequest request) { return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken()); } private String createNewToken() { return UUID.randomUUID().toString(); } }
這段源碼其實也很好理解:
這是默認的方案,適用於先後端不分的開發,具體用法能夠參考上篇文章。
若是想在先後端分離開發中使用,那就須要 CsrfTokenRepository 的另外一個實現類 CookieCsrfTokenRepository ,代碼以下:
public final class CookieCsrfTokenRepository implements CsrfTokenRepository { static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN"; static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf"; static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN"; private String parameterName = DEFAULT_CSRF_PARAMETER_NAME; private String headerName = DEFAULT_CSRF_HEADER_NAME; private String cookieName = DEFAULT_CSRF_COOKIE_NAME; private boolean cookieHttpOnly = true; private String cookiePath; private String cookieDomain; public CookieCsrfTokenRepository() { } @Override public CsrfToken generateToken(HttpServletRequest request) { return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken()); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { String tokenValue = token == null ? "" : token.getToken(); Cookie cookie = new Cookie(this.cookieName, tokenValue); cookie.setSecure(request.isSecure()); if (this.cookiePath != null && !this.cookiePath.isEmpty()) { cookie.setPath(this.cookiePath); } else { cookie.setPath(this.getRequestContext(request)); } if (token == null) { cookie.setMaxAge(0); } else { cookie.setMaxAge(-1); } cookie.setHttpOnly(cookieHttpOnly); if (this.cookieDomain != null && !this.cookieDomain.isEmpty()) { cookie.setDomain(this.cookieDomain); } response.addCookie(cookie); } @Override public CsrfToken loadToken(HttpServletRequest request) { Cookie cookie = WebUtils.getCookie(request, this.cookieName); if (cookie == null) { return null; } String token = cookie.getValue(); if (!StringUtils.hasLength(token)) { return null; } return new DefaultCsrfToken(this.headerName, this.parameterName, token); } public static CookieCsrfTokenRepository withHttpOnlyFalse() { CookieCsrfTokenRepository result = new CookieCsrfTokenRepository(); result.setCookieHttpOnly(false); return result; } private String createNewToken() { return UUID.randomUUID().toString(); } }
和 HttpSessionCsrfTokenRepository 相比,這裏 _csrf
數據保存的時候,都保存到 cookie 中去了,固然讀取的時候,也是從 cookie 中讀取,其餘地方則和 HttpSessionCsrfTokenRepository 是同樣的。
OK,這就是咱們整個 _csrf
參數生成的過程。
總結一下,就是生成一個 CsrfToken,這個 Token,本質上就是一個 UUID 字符串,而後將這個 Token 保存到 HttpSession 中,或者保存到 Cookie 中,待請求到來時,從 HttpSession 或者 Cookie 中取出來作校驗。
那接下來就是校驗了。
校驗主要是經過 CsrfFilter 過濾器來進行,咱們來看下核心的 doFilterInternal 方法:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(HttpServletResponse.class.getName(), response); CsrfToken csrfToken = this.tokenRepository.loadToken(request); final 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)) { filterChain.doFilter(request, response); return; } String actualToken = request.getHeader(csrfToken.getHeaderName()); 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)); } if (missingToken) { this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken)); } else { this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken)); } return; } filterChain.doFilter(request, response); }
這個方法我來稍微解釋下:
_csrf
的數據來源。如此以後,就完成了整個校驗工做了。
前面咱們說了 CsrfTokenRepository 有四個實現類,除了咱們介紹的兩個以外,還有一個 LazyCsrfTokenRepository,這裏鬆哥也和你們作一個簡單介紹。
在前面的 CsrfFilter 中你們發現,對於常見的 GET 請求其實是不須要 CSRF 攻擊校驗的,可是,每當 GET 請求到來時,下面這段代碼都會執行:
if (missingToken) { csrfToken = this.tokenRepository.generateToken(request); this.tokenRepository.saveToken(csrfToken, request, response); }
生成 CsrfToken 並保存,但實際上卻沒什麼用,由於 GET 請求不須要 CSRF 攻擊校驗。
因此,Spring Security 官方又推出了 LazyCsrfTokenRepository。
LazyCsrfTokenRepository 實際上不能算是一個真正的 CsrfTokenRepository,它是一個代理,能夠用來加強 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的功能:
public final class LazyCsrfTokenRepository implements CsrfTokenRepository { @Override public CsrfToken generateToken(HttpServletRequest request) { return wrap(request, this.delegate.generateToken(request)); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { if (token == null) { this.delegate.saveToken(token, request, response); } } @Override public CsrfToken loadToken(HttpServletRequest request) { return this.delegate.loadToken(request); } private CsrfToken wrap(HttpServletRequest request, CsrfToken token) { HttpServletResponse response = getResponse(request); return new SaveOnAccessCsrfToken(this.delegate, request, response, token); } private static final class SaveOnAccessCsrfToken implements CsrfToken { private transient CsrfTokenRepository tokenRepository; private transient HttpServletRequest request; private transient HttpServletResponse response; private final CsrfToken delegate; SaveOnAccessCsrfToken(CsrfTokenRepository tokenRepository, HttpServletRequest request, HttpServletResponse response, CsrfToken delegate) { this.tokenRepository = tokenRepository; this.request = request; this.response = response; this.delegate = delegate; } @Override public String getToken() { saveTokenIfNecessary(); return this.delegate.getToken(); } private void saveTokenIfNecessary() { if (this.tokenRepository == null) { return; } synchronized (this) { if (this.tokenRepository != null) { this.tokenRepository.saveToken(this.delegate, this.request, this.response); this.tokenRepository = null; this.request = null; this.response = null; } } } } }
這裏,我說三點:
使用了 LazyCsrfTokenRepository 以後,只有在使用 csrfToken 時纔會去存儲它,這樣就能夠節省存儲空間了。
LazyCsrfTokenRepository 的配置方式也很簡單,在咱們使用 Spring Security 時,若是對 csrf 不作任何配置,默認其實就是 LazyCsrfTokenRepository+HttpSessionCsrfTokenRepository 組合。
固然咱們也能夠本身配置,以下:
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html") .successHandler((req,resp,authentication)->{ resp.getWriter().write("success"); }) .permitAll() .and() .csrf().csrfTokenRepository(new LazyCsrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())); }
今天主要和小夥伴聊了一下 Spring Security 中 csrf 防護的原理。
總體來講,就是兩個思路:
好啦,不知道小夥伴們有沒有 GET 到呢?若是以爲有收穫,記得點個在看鼓勵下鬆哥哦~