Spring Security 中 CSRF 防御源碼解析
上篇文章松哥和大家聊了什么是 CSRF 攻擊,以及 CSRF 攻擊要如何防御。主要和大家聊了 Spring Security 中處理該問題的幾種辦法。
今天松哥來和大家簡(jiǎn)單的看一下 Spring Security 中,CSRF 防御源碼。
本文主要從兩個(gè)方面來和大家講解:
- 返回給前端的 _csrf 參數(shù)是如何生成的。
 - 前端傳來的 _csrf 參數(shù)是如何校驗(yàn)的。
 
1.隨機(jī)字符串生成
我們先來看一下 Spring Security 中的 csrf 參數(shù)是如何生成的。
首先,Spring Security 中提供了一個(gè)保存 csrf 參數(shù)的規(guī)范,就是 CsrfToken:
- public interface CsrfToken extends Serializable {
 - String getHeaderName();
 - String getParameterName();
 - String getToken();
 - }
 
這里三個(gè)方法都好理解,前兩個(gè)是獲取 _csrf 參數(shù)的 key,第三個(gè)是獲取 _csrf 參數(shù)的 value。
CsrfToken 有兩個(gè)實(shí)現(xiàn)類,如下:
默認(rèn)情況下使用的是 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;
 - }
 - }
 
這段實(shí)現(xiàn)很簡(jiǎn)單,幾乎沒有添加額外的方法,就是接口方法的實(shí)現(xiàn)。
CsrfToken 相當(dāng)于就是 _csrf 參數(shù)的載體。那么參數(shù)是如何生成和保存的呢?這涉及到另外一個(gè)類:
- public interface CsrfTokenRepository {
 - CsrfToken generateToken(HttpServletRequest request);
 - void saveToken(CsrfToken token, HttpServletRequest request,
 - HttpServletResponse response);
 - CsrfToken loadToken(HttpServletRequest request);
 - }
 
這里三個(gè)方法:
- generateToken 方法就是 CsrfToken 的生成過程。
 - saveToken 方法就是保存 CsrfToken。
 - loadToken 則是如何加載 CsrfToken。
 
CsrfTokenRepository 有四個(gè)實(shí)現(xiàn)類,在上篇文章中,我們用到了其中兩個(gè):HttpSessionCsrfTokenRepository 和 CookieCsrfTokenRepository,其中 HttpSessionCsrfTokenRepository 是默認(rèn)的方案。
我們先來看下 HttpSessionCsrfTokenRepository 的實(shí)現(xiàn):
- 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();
 - }
 - }
 
這段源碼其實(shí)也很好理解:
- saveToken 方法將 CsrfToken 保存在 HttpSession 中,將來再?gòu)?HttpSession 中取出和前端傳來的參數(shù)做比較。
 - loadToken 方法當(dāng)然就是從 HttpSession 中讀取 CsrfToken 出來。
 - generateToken 是生成 CsrfToken 的過程,可以看到,生成的默認(rèn)載體就是 DefaultCsrfToken,而 CsrfToken 的值則通過 createNewToken 方法生成,是一個(gè) UUID 字符串。
 - 在構(gòu)造 DefaultCsrfToken 是還有兩個(gè)參數(shù) headerName 和 parameterName,這兩個(gè)參數(shù)是前端保存參數(shù)的 key。
 
這是默認(rèn)的方案,適用于前后端不分的開發(fā),具體用法可以參考上篇文章。
如果想在前后端分離開發(fā)中使用,那就需要 CsrfTokenRepository 的另一個(gè)實(shí)現(xiàn)類 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 數(shù)據(jù)保存的時(shí)候,都保存到 cookie 中去了,當(dāng)然讀取的時(shí)候,也是從 cookie 中讀取,其他地方則和 HttpSessionCsrfTokenRepository 是一樣的。
OK,這就是我們整個(gè) _csrf 參數(shù)生成的過程。
總結(jié)一下,就是生成一個(gè) CsrfToken,這個(gè) Token,本質(zhì)上就是一個(gè) UUID 字符串,然后將這個(gè) Token 保存到 HttpSession 中,或者保存到 Cookie 中,待請(qǐng)求到來時(shí),從 HttpSession 或者 Cookie 中取出來做校驗(yàn)。
2.參數(shù)校驗(yàn)
那接下來就是校驗(yàn)了。
校驗(yàn)主要是通過 CsrfFilter 過濾器來進(jìn)行,我們來看下核心的 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);
 - }
 
這個(gè)方法我來稍微解釋下:
- 首先調(diào)用 tokenRepository.loadToken 方法讀取 CsrfToken 出來,這個(gè) tokenRepository 就是你配置的 CsrfTokenRepository 實(shí)例,CsrfToken 存在 HttpSession 中,這里就從 HttpSession 中讀取,CsrfToken 存在 Cookie 中,這里就從 Cookie 中讀取。
 - 如果調(diào)用 tokenRepository.loadToken 方法沒有加載到 CsrfToken,那說明這個(gè)請(qǐng)求可能是第一次發(fā)起,則調(diào)用 tokenRepository.generateToken 方法生成 CsrfToken ,并調(diào)用 tokenRepository.saveToken 方法保存 CsrfToken。
 - 大家注意,這里還調(diào)用 request.setAttribute 方法存了一些值進(jìn)去,這就是默認(rèn)情況下,我們通過 jsp 或者 thymeleaf 標(biāo)簽渲染 _csrf 的數(shù)據(jù)來源。
 - requireCsrfProtectionMatcher.matches 方法則使用用來判斷哪些請(qǐng)求方法需要做校驗(yàn),默認(rèn)情況下,"GET", "HEAD", "TRACE", "OPTIONS" 方法是不需要校驗(yàn)的。
 - 接下來獲取請(qǐng)求中傳遞來的 CSRF 參數(shù),先從請(qǐng)求頭中獲取,獲取不到再?gòu)恼?qǐng)求參數(shù)中獲取。
 - 獲取到請(qǐng)求傳來的 csrf 參數(shù)之后,再和一開始加載到的 csrfToken 做比較,如果不同的話,就拋出異常。
 
如此之后,就完成了整個(gè)校驗(yàn)工作了。
3.LazyCsrfTokenRepository
前面我們說了 CsrfTokenRepository 有四個(gè)實(shí)現(xiàn)類,除了我們介紹的兩個(gè)之外,還有一個(gè) LazyCsrfTokenRepository,這里松哥也和大家做一個(gè)簡(jiǎn)單介紹。
在前面的 CsrfFilter 中大家發(fā)現(xiàn),對(duì)于常見的 GET 請(qǐng)求實(shí)際上是不需要 CSRF 攻擊校驗(yàn)的,但是,每當(dāng) GET 請(qǐng)求到來時(shí),下面這段代碼都會(huì)執(zhí)行:
- if (missingToken) {
 - csrfToken = this.tokenRepository.generateToken(request);
 - this.tokenRepository.saveToken(csrfToken, request, response);
 - }
 
生成 CsrfToken 并保存,但實(shí)際上卻沒什么用,因?yàn)?GET 請(qǐng)求不需要 CSRF 攻擊校驗(yàn)。
所以,Spring Security 官方又推出了 LazyCsrfTokenRepository。
LazyCsrfTokenRepository 實(shí)際上不能算是一個(gè)真正的 CsrfTokenRepository,它是一個(gè)代理,可以用來增強(qiáng) 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;
 - }
 - }
 - }
 - }
 - }
 
這里,我說三點(diǎn):
- generateToken 方法,該方法用來生成 CsrfToken,默認(rèn) CsrfToken 的載體是 DefaultCsrfToken,現(xiàn)在換成了 SaveOnAccessCsrfToken。
 - SaveOnAccessCsrfToken 和 DefaultCsrfToken 并沒有太大區(qū)別,主要是 getToken 方法有區(qū)別,在 SaveOnAccessCsrfToken 中,當(dāng)開發(fā)者調(diào)用 getToken 想要去獲取 csrfToken 時(shí),才會(huì)去對(duì) csrfToken 做保存操作(調(diào)用 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的 saveToken 方法)。
 - LazyCsrfTokenRepository 自己的 saveToken 則做了修改,相當(dāng)于放棄了 saveToken 的功能,調(diào)用該方法并不會(huì)做保存操作。
 
使用了 LazyCsrfTokenRepository 之后,只有在使用 csrfToken 時(shí)才會(huì)去存儲(chǔ)它,這樣就可以節(jié)省存儲(chǔ)空間了。
LazyCsrfTokenRepository 的配置方式也很簡(jiǎn)單,在我們使用 Spring Security 時(shí),如果對(duì) csrf 不做任何配置,默認(rèn)其實(shí)就是 LazyCsrfTokenRepository+HttpSessionCsrfTokenRepository 組合。
當(dāng)然我們也可以自己配置,如下:
- @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()));
 - }
 
4.小結(jié)
今天主要和小伙伴聊了一下 Spring Security 中 csrf 防御的原理。
整體來說,就是兩個(gè)思路:
生成 csrfToken 保存在 HttpSession 或者 Cookie 中。
請(qǐng)求到來時(shí),從請(qǐng)求中提取出來 csrfToken,和保存的 csrfToken 做比較,進(jìn)而判斷出當(dāng)前請(qǐng)求是否合法。
本文轉(zhuǎn)載自微信公眾號(hào)「江南一點(diǎn)雨」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系江南一點(diǎn)雨公眾號(hào)。



















 
 
 






 
 
 
 