成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Spring Security 中 CSRF 防御源碼解析

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

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

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

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

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

1.隨機字符串生成

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

首先,Spring Security 中提供了一個保存 csrf 參數的規范,就是 CsrfToken:

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

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

CsrfToken 有兩個實現類,如下:

默認情況下使用的是 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.  } 

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

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

  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 有四個實現類,在上篇文章中,我們用到了其中兩個:HttpSessionCsrfTokenRepository 和 CookieCsrfTokenRepository,其中 HttpSessionCsrfTokenRepository 是默認的方案。

我們先來看下 HttpSessionCsrfTokenRepository 的實現:

  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 中取出和前端傳來的參數做比較。
  2. loadToken 方法當然就是從 HttpSession 中讀取 CsrfToken 出來。
  3. generateToken 是生成 CsrfToken 的過程,可以看到,生成的默認載體就是 DefaultCsrfToken,而 CsrfToken 的值則通過 createNewToken 方法生成,是一個 UUID 字符串。
  4. 在構造 DefaultCsrfToken 是還有兩個參數 headerName 和 parameterName,這兩個參數是前端保存參數的 key。

這是默認的方案,適用于前后端不分的開發,具體用法可以參考上篇文章

如果想在前后端分離開發中使用,那就需要 CsrfTokenRepository 的另一個實現類 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 數據保存的時候,都保存到 cookie 中去了,當然讀取的時候,也是從 cookie 中讀取,其他地方則和 HttpSessionCsrfTokenRepository 是一樣的。

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

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

2.參數校驗

那接下來就是校驗了。

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

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

3.LazyCsrfTokenRepository

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

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

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

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

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

LazyCsrfTokenRepository 實際上不能算是一個真正的 CsrfTokenRepository,它是一個代理,可以用來增強 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,默認 CsrfToken 的載體是 DefaultCsrfToken,現在換成了 SaveOnAccessCsrfToken。
  2. SaveOnAccessCsrfToken 和 DefaultCsrfToken 并沒有太大區別,主要是 getToken 方法有區別,在 SaveOnAccessCsrfToken 中,當開發者調用 getToken 想要去獲取 csrfToken 時,才會去對 csrfToken 做保存操作(調用 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的 saveToken 方法)。
  3. LazyCsrfTokenRepository 自己的 saveToken 則做了修改,相當于放棄了 saveToken 的功能,調用該方法并不會做保存操作。

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

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

當然我們也可以自己配置,如下:

  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.小結

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

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

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

請求到來時,從請求中提取出來 csrfToken,和保存的 csrfToken 做比較,進而判斷出當前請求是否合法。

本文轉載自微信公眾號「江南一點雨」,可以通過以下二維碼關注。轉載本文請聯系江南一點雨公眾號。

 

責任編輯:武曉燕 來源: 江南一點雨
相關推薦

2021-06-03 10:16:12

CSRF攻擊SpringBoot

2022-05-19 11:29:14

計時攻擊SpringSecurity

2016-09-30 15:59:41

2021-04-28 06:26:11

Spring Secu功能實現源碼分析

2021-04-23 07:33:10

SpringSecurity單元

2016-09-21 10:11:19

2021-04-19 07:57:23

Spring 源碼GetBean

2013-05-22 18:32:57

2022-12-07 08:02:43

Spring流程IOC

2020-09-16 08:07:54

權限粒度Spring Secu

2023-11-03 07:58:54

CORSSpring

2022-11-26 00:00:02

2021-08-29 18:36:57

項目

2020-09-02 08:09:10

攻擊防御Shiro

2022-05-05 10:40:36

Spring權限對象

2011-05-16 14:26:28

2017-05-16 10:39:02

2020-06-17 08:31:10

權限控制Spring Secu

2021-07-27 10:49:10

SpringSecurity權限

2022-08-17 07:52:31

Spring循環依賴單例池
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲免费三区 | 久久免费观看视频 | 久久成人一区 | 欧产日产国产精品视频 | 啪啪精品 | 精品久久一区二区三区 | 亚洲综合在线视频 | 日本一本视频 | 亚洲精品乱码8久久久久久日本 | 一本综合久久 | 亚洲精品国产电影 | 国产精品特级毛片一区二区三区 | 国产一级淫片免费视频 | 99亚洲精品 | 日韩高清一区 | 亚州成人 | 国产精品免费小视频 | 天天看天天干 | 久久久久国产一级毛片高清网站 | 国产亚洲精品美女久久久久久久久久 | 久久99精品久久久久 | 亚洲综合在线一区二区 | 狠狠色香婷婷久久亚洲精品 | 亚洲成人福利在线观看 | 亚州综合一区 | 不卡在线视频 | 成人国产一区二区三区精品麻豆 | 国内精品一区二区三区 | 免费看黄色小视频 | 国产特一级黄色片 | 日韩精品免费播放 | www国产成人免费观看视频,深夜成人网 | 在线一级片| 日韩资源 | 成人在线一区二区 | 成人性视频在线播放 | 久久亚洲国产 | 国产成人精品久久二区二区 | 中文字幕国 | 天天射网站 | 亚洲精品久久久久久久不卡四虎 |