自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Spring Security 中 CSRF 防御源碼解析

安全 應(yīng)用安全
就是生成一個 CsrfToken,這個 Token,本質(zhì)上就是一個 UUID 字符串,然后將這個 Token 保存到 HttpSession 中,或者保存到 Cookie 中,待請求到來時,從 HttpSession 或者 Cookie 中取出來做校驗。

 上篇文章松哥和大家聊了什么是 CSRF 攻擊,以及 CSRF 攻擊要如何防御。主要和大家聊了 Spring Security 中處理該問題的幾種辦法。

今天松哥來和大家簡單的看一下 Spring Security 中,CSRF 防御源碼。

本文主要從兩個方面來和大家講解:

  • 返回給前端的 _csrf 參數(shù)是如何生成的。
  • 前端傳來的 _csrf 參數(shù)是如何校驗的。

1.隨機(jī)字符串生成

我們先來看一下 Spring Security 中的 csrf 參數(shù)是如何生成的。

首先,Spring Security 中提供了一個保存 csrf 參數(shù)的規(guī)范,就是 CsrfToken:

  1. public interface CsrfToken extends Serializable { 
  2.  String getHeaderName(); 
  3.  String getParameterName(); 
  4.  String getToken(); 
  5.  

這里三個方法都好理解,前兩個是獲取 _csrf 參數(shù)的 key,第三個是獲取 _csrf 參數(shù)的 value。

CsrfToken 有兩個實現(xiàn)類,如下:

默認(rèn)情況下使用的是 DefaultCsrfToken,我們來稍微看下 DefaultCsrfToken:

  1. public final class DefaultCsrfToken implements CsrfToken { 
  2.  private final String token; 
  3.  private final String parameterName; 
  4.  private final String headerName; 
  5.  public DefaultCsrfToken(String headerName, String parameterName, String token) { 
  6.   this.headerName = headerName; 
  7.   this.parameterName = parameterName; 
  8.   this.token = token; 
  9.  } 
  10.  public String getHeaderName() { 
  11.   return this.headerName; 
  12.  } 
  13.  public String getParameterName() { 
  14.   return this.parameterName; 
  15.  } 
  16.  public String getToken() { 
  17.   return this.token; 
  18.  } 

這段實現(xiàn)很簡單,幾乎沒有添加額外的方法,就是接口方法的實現(xiàn)。

CsrfToken 相當(dāng)于就是 _csrf 參數(shù)的載體。那么參數(shù)是如何生成和保存的呢?這涉及到另外一個類:

  1. public interface CsrfTokenRepository { 
  2.  CsrfToken generateToken(HttpServletRequest request); 
  3.  void saveToken(CsrfToken token, HttpServletRequest request, 
  4.    HttpServletResponse response); 
  5.  CsrfToken loadToken(HttpServletRequest request); 

這里三個方法:

  1. generateToken 方法就是 CsrfToken 的生成過程。
  2. saveToken 方法就是保存 CsrfToken。
  3. loadToken 則是如何加載 CsrfToken。

CsrfTokenRepository 有四個實現(xiàn)類,在上篇文章中,我們用到了其中兩個:HttpSessionCsrfTokenRepository 和 CookieCsrfTokenRepository,其中 HttpSessionCsrfTokenRepository 是默認(rèn)的方案。

我們先來看下 HttpSessionCsrfTokenRepository 的實現(xiàn):

  1. public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository { 
  2.  private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf"
  3.  private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN"
  4.  private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class 
  5.    .getName().concat(".CSRF_TOKEN"); 
  6.  private String parameterName = DEFAULT_CSRF_PARAMETER_NAME; 
  7.  private String headerName = DEFAULT_CSRF_HEADER_NAME; 
  8.  private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME; 
  9.  public void saveToken(CsrfToken token, HttpServletRequest request, 
  10.    HttpServletResponse response) { 
  11.   if (token == null) { 
  12.    HttpSession session = request.getSession(false); 
  13.    if (session != null) { 
  14.     session.removeAttribute(this.sessionAttributeName); 
  15.    } 
  16.   } 
  17.   else { 
  18.    HttpSession session = request.getSession(); 
  19.    session.setAttribute(this.sessionAttributeName, token); 
  20.   } 
  21.  } 
  22.  public CsrfToken loadToken(HttpServletRequest request) { 
  23.   HttpSession session = request.getSession(false); 
  24.   if (session == null) { 
  25.    return null
  26.   } 
  27.   return (CsrfToken) session.getAttribute(this.sessionAttributeName); 
  28.  } 
  29.  public CsrfToken generateToken(HttpServletRequest request) { 
  30.   return new DefaultCsrfToken(this.headerName, this.parameterName, 
  31.     createNewToken()); 
  32.  } 
  33.  private String createNewToken() { 
  34.   return UUID.randomUUID().toString(); 
  35.  } 

這段源碼其實也很好理解:

  1. saveToken 方法將 CsrfToken 保存在 HttpSession 中,將來再從 HttpSession 中取出和前端傳來的參數(shù)做比較。
  2. loadToken 方法當(dāng)然就是從 HttpSession 中讀取 CsrfToken 出來。
  3. generateToken 是生成 CsrfToken 的過程,可以看到,生成的默認(rèn)載體就是 DefaultCsrfToken,而 CsrfToken 的值則通過 createNewToken 方法生成,是一個 UUID 字符串。
  4. 在構(gòu)造 DefaultCsrfToken 是還有兩個參數(shù) headerName 和 parameterName,這兩個參數(shù)是前端保存參數(shù)的 key。

這是默認(rèn)的方案,適用于前后端不分的開發(fā),具體用法可以參考上篇文章。

如果想在前后端分離開發(fā)中使用,那就需要 CsrfTokenRepository 的另一個實現(xiàn)類 CookieCsrfTokenRepository ,代碼如下:

  1. public final class CookieCsrfTokenRepository implements CsrfTokenRepository { 
  2.  static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN"
  3.  static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf"
  4.  static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN"
  5.  private String parameterName = DEFAULT_CSRF_PARAMETER_NAME; 
  6.  private String headerName = DEFAULT_CSRF_HEADER_NAME; 
  7.  private String cookieName = DEFAULT_CSRF_COOKIE_NAME; 
  8.  private boolean cookieHttpOnly = true
  9.  private String cookiePath; 
  10.  private String cookieDomain; 
  11.  public CookieCsrfTokenRepository() { 
  12.  } 
  13.  @Override 
  14.  public CsrfToken generateToken(HttpServletRequest request) { 
  15.   return new DefaultCsrfToken(this.headerName, this.parameterName, 
  16.     createNewToken()); 
  17.  } 
  18.  @Override 
  19.  public void saveToken(CsrfToken token, HttpServletRequest request, 
  20.    HttpServletResponse response) { 
  21.   String tokenValue = token == null ? "" : token.getToken(); 
  22.   Cookie cookie = new Cookie(this.cookieName, tokenValue); 
  23.   cookie.setSecure(request.isSecure()); 
  24.   if (this.cookiePath != null && !this.cookiePath.isEmpty()) { 
  25.     cookie.setPath(this.cookiePath); 
  26.   } else { 
  27.     cookie.setPath(this.getRequestContext(request)); 
  28.   } 
  29.   if (token == null) { 
  30.    cookie.setMaxAge(0); 
  31.   } 
  32.   else { 
  33.    cookie.setMaxAge(-1); 
  34.   } 
  35.   cookie.setHttpOnly(cookieHttpOnly); 
  36.   if (this.cookieDomain != null && !this.cookieDomain.isEmpty()) { 
  37.    cookie.setDomain(this.cookieDomain); 
  38.   } 
  39.  
  40.   response.addCookie(cookie); 
  41.  } 
  42.  @Override 
  43.  public CsrfToken loadToken(HttpServletRequest request) { 
  44.   Cookie cookie = WebUtils.getCookie(request, this.cookieName); 
  45.   if (cookie == null) { 
  46.    return null
  47.   } 
  48.   String token = cookie.getValue(); 
  49.   if (!StringUtils.hasLength(token)) { 
  50.    return null
  51.   } 
  52.   return new DefaultCsrfToken(this.headerName, this.parameterName, token); 
  53.  } 
  54.  public static CookieCsrfTokenRepository withHttpOnlyFalse() { 
  55.   CookieCsrfTokenRepository result = new CookieCsrfTokenRepository(); 
  56.   result.setCookieHttpOnly(false); 
  57.   return result; 
  58.  } 
  59.  private String createNewToken() { 
  60.   return UUID.randomUUID().toString(); 
  61.  } 

和 HttpSessionCsrfTokenRepository 相比,這里 _csrf 數(shù)據(jù)保存的時候,都保存到 cookie 中去了,當(dāng)然讀取的時候,也是從 cookie 中讀取,其他地方則和 HttpSessionCsrfTokenRepository 是一樣的。

OK,這就是我們整個 _csrf 參數(shù)生成的過程。

總結(jié)一下,就是生成一個 CsrfToken,這個 Token,本質(zhì)上就是一個 UUID 字符串,然后將這個 Token 保存到 HttpSession 中,或者保存到 Cookie 中,待請求到來時,從 HttpSession 或者 Cookie 中取出來做校驗。

2.參數(shù)校驗

那接下來就是校驗了。

校驗主要是通過 CsrfFilter 過濾器來進(jìn)行,我們來看下核心的 doFilterInternal 方法:

  1. protected void doFilterInternal(HttpServletRequest request, 
  2.   HttpServletResponse response, FilterChain filterChain) 
  3.     throws ServletException, IOException { 
  4.  request.setAttribute(HttpServletResponse.class.getName(), response); 
  5.  CsrfToken csrfToken = this.tokenRepository.loadToken(request); 
  6.  final boolean missingToken = csrfToken == null
  7.  if (missingToken) { 
  8.   csrfToken = this.tokenRepository.generateToken(request); 
  9.   this.tokenRepository.saveToken(csrfToken, request, response); 
  10.  } 
  11.  request.setAttribute(CsrfToken.class.getName(), csrfToken); 
  12.  request.setAttribute(csrfToken.getParameterName(), csrfToken); 
  13.  if (!this.requireCsrfProtectionMatcher.matches(request)) { 
  14.   filterChain.doFilter(request, response); 
  15.   return
  16.  } 
  17.  String actualToken = request.getHeader(csrfToken.getHeaderName()); 
  18.  if (actualToken == null) { 
  19.   actualToken = request.getParameter(csrfToken.getParameterName()); 
  20.  } 
  21.  if (!csrfToken.getToken().equals(actualToken)) { 
  22.   if (this.logger.isDebugEnabled()) { 
  23.    this.logger.debug("Invalid CSRF token found for " 
  24.      + UrlUtils.buildFullRequestUrl(request)); 
  25.   } 
  26.   if (missingToken) { 
  27.    this.accessDeniedHandler.handle(request, response, 
  28.      new MissingCsrfTokenException(actualToken)); 
  29.   } 
  30.   else { 
  31.    this.accessDeniedHandler.handle(request, response, 
  32.      new InvalidCsrfTokenException(csrfToken, actualToken)); 
  33.   } 
  34.   return
  35.  } 
  36.  filterChain.doFilter(request, response); 

這個方法我來稍微解釋下:

  1. 首先調(diào)用 tokenRepository.loadToken 方法讀取 CsrfToken 出來,這個 tokenRepository 就是你配置的 CsrfTokenRepository 實例,CsrfToken 存在 HttpSession 中,這里就從 HttpSession 中讀取,CsrfToken 存在 Cookie 中,這里就從 Cookie 中讀取。
  2. 如果調(diào)用 tokenRepository.loadToken 方法沒有加載到 CsrfToken,那說明這個請求可能是第一次發(fā)起,則調(diào)用 tokenRepository.generateToken 方法生成 CsrfToken ,并調(diào)用 tokenRepository.saveToken 方法保存 CsrfToken。
  3. 大家注意,這里還調(diào)用 request.setAttribute 方法存了一些值進(jìn)去,這就是默認(rèn)情況下,我們通過 jsp 或者 thymeleaf 標(biāo)簽渲染 _csrf 的數(shù)據(jù)來源。
  4. requireCsrfProtectionMatcher.matches 方法則使用用來判斷哪些請求方法需要做校驗,默認(rèn)情況下,"GET", "HEAD", "TRACE", "OPTIONS" 方法是不需要校驗的。
  5. 接下來獲取請求中傳遞來的 CSRF 參數(shù),先從請求頭中獲取,獲取不到再從請求參數(shù)中獲取。
  6. 獲取到請求傳來的 csrf 參數(shù)之后,再和一開始加載到的 csrfToken 做比較,如果不同的話,就拋出異常。

如此之后,就完成了整個校驗工作了。

3.LazyCsrfTokenRepository

前面我們說了 CsrfTokenRepository 有四個實現(xiàn)類,除了我們介紹的兩個之外,還有一個 LazyCsrfTokenRepository,這里松哥也和大家做一個簡單介紹。

在前面的 CsrfFilter 中大家發(fā)現(xiàn),對于常見的 GET 請求實際上是不需要 CSRF 攻擊校驗的,但是,每當(dāng) GET 請求到來時,下面這段代碼都會執(zhí)行:

  1. if (missingToken) { 
  2.  csrfToken = this.tokenRepository.generateToken(request); 
  3.  this.tokenRepository.saveToken(csrfToken, request, response); 

生成 CsrfToken 并保存,但實際上卻沒什么用,因為 GET 請求不需要 CSRF 攻擊校驗。

所以,Spring Security 官方又推出了 LazyCsrfTokenRepository。

LazyCsrfTokenRepository 實際上不能算是一個真正的 CsrfTokenRepository,它是一個代理,可以用來增強(qiáng) HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的功能:

  1. public final class LazyCsrfTokenRepository implements CsrfTokenRepository { 
  2.  @Override 
  3.  public CsrfToken generateToken(HttpServletRequest request) { 
  4.   return wrap(request, this.delegate.generateToken(request)); 
  5.  } 
  6.  @Override 
  7.  public void saveToken(CsrfToken token, HttpServletRequest request, 
  8.    HttpServletResponse response) { 
  9.   if (token == null) { 
  10.    this.delegate.saveToken(token, request, response); 
  11.   } 
  12.  } 
  13.  @Override 
  14.  public CsrfToken loadToken(HttpServletRequest request) { 
  15.   return this.delegate.loadToken(request); 
  16.  } 
  17.  private CsrfToken wrap(HttpServletRequest request, CsrfToken token) { 
  18.   HttpServletResponse response = getResponse(request); 
  19.   return new SaveOnAccessCsrfToken(this.delegate, request, response, token); 
  20.  } 
  21.  private static final class SaveOnAccessCsrfToken implements CsrfToken { 
  22.   private transient CsrfTokenRepository tokenRepository; 
  23.   private transient HttpServletRequest request; 
  24.   private transient HttpServletResponse response; 
  25.  
  26.   private final CsrfToken delegate; 
  27.  
  28.   SaveOnAccessCsrfToken(CsrfTokenRepository tokenRepository, 
  29.     HttpServletRequest request, HttpServletResponse response, 
  30.     CsrfToken delegate) { 
  31.    this.tokenRepository = tokenRepository; 
  32.    this.request = request; 
  33.    this.response = response; 
  34.    this.delegate = delegate; 
  35.   } 
  36.   @Override 
  37.   public String getToken() { 
  38.    saveTokenIfNecessary(); 
  39.    return this.delegate.getToken(); 
  40.   } 
  41.   private void saveTokenIfNecessary() { 
  42.    if (this.tokenRepository == null) { 
  43.     return
  44.    } 
  45.  
  46.    synchronized (this) { 
  47.     if (this.tokenRepository != null) { 
  48.      this.tokenRepository.saveToken(this.delegate, this.request, 
  49.        this.response); 
  50.      this.tokenRepository = null
  51.      this.request = null
  52.      this.response = null
  53.     } 
  54.    } 
  55.   } 
  56.  
  57.  } 

這里,我說三點:

  1. generateToken 方法,該方法用來生成 CsrfToken,默認(rèn) CsrfToken 的載體是 DefaultCsrfToken,現(xiàn)在換成了 SaveOnAccessCsrfToken。
  2. SaveOnAccessCsrfToken 和 DefaultCsrfToken 并沒有太大區(qū)別,主要是 getToken 方法有區(qū)別,在 SaveOnAccessCsrfToken 中,當(dāng)開發(fā)者調(diào)用 getToken 想要去獲取 csrfToken 時,才會去對 csrfToken 做保存操作(調(diào)用 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的 saveToken 方法)。
  3. LazyCsrfTokenRepository 自己的 saveToken 則做了修改,相當(dāng)于放棄了 saveToken 的功能,調(diào)用該方法并不會做保存操作。

使用了 LazyCsrfTokenRepository 之后,只有在使用 csrfToken 時才會去存儲它,這樣就可以節(jié)省存儲空間了。

LazyCsrfTokenRepository 的配置方式也很簡單,在我們使用 Spring Security 時,如果對 csrf 不做任何配置,默認(rèn)其實就是 LazyCsrfTokenRepository+HttpSessionCsrfTokenRepository 組合。

當(dāng)然我們也可以自己配置,如下:

  1. @Override 
  2. protected void configure(HttpSecurity http) throws Exception { 
  3.     http.authorizeRequests().anyRequest().authenticated() 
  4.             .and() 
  5.             .formLogin() 
  6.             .loginPage("/login.html"
  7.             .successHandler((req,resp,authentication)->{ 
  8.                 resp.getWriter().write("success"); 
  9.             }) 
  10.             .permitAll() 
  11.             .and() 
  12.             .csrf().csrfTokenRepository(new LazyCsrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())); 

4.小結(jié)

今天主要和小伙伴聊了一下 Spring Security 中 csrf 防御的原理。

整體來說,就是兩個思路:

生成 csrfToken 保存在 HttpSession 或者 Cookie 中。

請求到來時,從請求中提取出來 csrfToken,和保存的 csrfToken 做比較,進(jìn)而判斷出當(dāng)前請求是否合法。

本文轉(zhuǎn)載自微信公眾號「江南一點雨」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系江南一點雨公眾號。

 

責(zé)任編輯:武曉燕 來源: 江南一點雨
相關(guān)推薦

2021-06-03 10:16:12

CSRF攻擊SpringBoot

2022-05-19 11:29:14

計時攻擊SpringSecurity

2021-04-28 06:26:11

Spring Secu功能實現(xiàn)源碼分析

2016-09-30 15:59:41

2021-04-23 07:33:10

SpringSecurity單元

2021-04-19 07:57:23

Spring 源碼GetBean

2016-09-21 10:11:19

2022-12-07 08:02:43

Spring流程IOC

2013-05-22 18:32:57

2020-09-16 08:07:54

權(quán)限粒度Spring Secu

2023-11-03 07:58:54

CORSSpring

2022-11-26 00:00:02

2021-08-29 18:36:57

項目

2022-05-05 10:40:36

Spring權(quán)限對象

2020-09-02 08:09:10

攻擊防御Shiro

2011-05-16 14:26:28

2022-08-17 07:52:31

Spring循環(huán)依賴單例池

2020-06-17 08:31:10

權(quán)限控制Spring Secu

2021-07-27 10:49:10

SpringSecurity權(quán)限

2022-08-30 08:50:07

Spring權(quán)限控制
點贊
收藏

51CTO技術(shù)棧公眾號