要學就學透徹!Spring Security 中 CSRF 防御源碼解析

      網友投稿 900 2025-03-31

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

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

      本文是本系列第 19 篇,閱讀本系列前面文章有助于更好的理解本文:

      挖一個大坑,Spring Security 開搞!

      松哥手把手帶你入門 Spring Security,別再問密碼怎么解密了

      手把手教你定制 Spring Security 中的表單登錄

      Spring Security 做前后端分離,咱就別做頁面跳轉了!統統 JSON 交互

      Spring Security 中的授權操作原來這么簡單

      Spring Security 如何將用戶數據存入數據庫?

      Spring Security+Spring Data Jpa 強強聯手,安全管理只有更簡單!

      Spring Boot + Spring Security 實現自動登錄功能

      Spring Boot 自動登錄,安全風險要怎么控制?

      在微服務項目中,Spring Security 比 Shiro 強在哪?

      SpringSecurity 自定義認證邏輯的兩種方式(高級玩法)

      Spring Security 中如何快速查看登錄用戶 IP 地址等信息?

      Spring Security 自動踢掉前一個登錄用戶,一個配置搞定!

      Spring Boot + Vue 前后端分離項目,如何踢掉已登錄用戶?

      Spring Security 自帶防火墻!你都不知道自己的系統有多安全!

      什么是會話固定攻擊?Spring Boot 中要如何防御會話固定攻擊?

      集群化部署,Spring Security 要如何處理 session 共享?

      松哥手把手教你在 SpringBoot 中防御 CSRF 攻擊!so easy!

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

      返回給前端的 _csrf 參數是如何生成的。

      前端傳來的 _csrf 參數是如何校驗的。

      1.隨機字符串生成

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

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

      public interface CsrfToken extends Serializable { String getHeaderName(); String getParameterName(); String getToken(); }

      1

      2

      3

      4

      5

      6

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

      CsrfToken 有兩個實現類,如下:

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

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

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

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

      public interface CsrfTokenRepository { CsrfToken generateToken(HttpServletRequest request); void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response); CsrfToken loadToken(HttpServletRequest request); }

      1

      2

      3

      4

      5

      6

      這里三個方法:

      generateToken 方法就是 CsrfToken 的生成過程。

      saveToken 方法就是保存 CsrfToken。

      loadToken 則是如何加載 CsrfToken。

      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(); } }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      36

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

      saveToken 方法將 CsrfToken 保存在 HttpSession 中,將來再從 HttpSession 中取出和前端傳來的參數做筆記。

      loadToken 方法當然就是從 HttpSession 中讀取 CsrfToken 出來。

      generateToken 是生成 CsrfToken 的過程,可以看到,生成的默認載體就是 DefaultCsrfToken,而 CsrfToken 的值則通過 createNewToken 方法生成,是一個 UUID 字符串。

      在構造 DefaultCsrfToken 是還有兩個參數 headerName 和 parameterName,這兩個參數是前端保存參數的 key。

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

      如果想在前后端分離開發中使用,那就需要 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(); } }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      36

      37

      38

      39

      40

      41

      42

      43

      44

      45

      46

      47

      48

      49

      50

      51

      52

      53

      54

      55

      56

      57

      58

      59

      60

      61

      62

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

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

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

      2.參數校驗

      那接下來就是校驗了。

      校驗主要是通過 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); }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      36

      37

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

      首先調用 tokenRepository.loadToken 方法讀取 CsrfToken 出來,這個 tokenRepository 就是你配置的 CsrfTokenRepository 實例,CsrfToken 存在 HttpSession 中,這里就從 HttpSession 中讀取,CsrfToken 存在 Cookie 中,這里就從 Cookie 中讀取。

      如果調用 tokenRepository.loadToken 方法沒有加載到 CsrfToken,那說明這個請求可能是第一次發起,則調用 tokenRepository.generateToken 方法生成 CsrfToken ,并調用 tokenRepository.saveToken 方法保存 CsrfToken。

      大家注意,這里還調用 request.setAttribute 方法存了一些值進去,這就是默認情況下,我們通過 jsp 或者 thymeleaf 標簽渲染 _csrf 的數據來源。

      requireCsrfProtectionMatcher.matches 方法則使用用來判斷哪些請求方法需要做校驗,默認情況下,“GET”, “HEAD”, “TRACE”, “OPTIONS” 方法是不需要校驗的。

      接下來獲取請求中傳遞來的 CSRF 參數,先從請求頭中獲取,獲取不到再從請求參數中獲取。

      獲取到請求傳來的 csrf 參數之后,再和一開始加載到的 csrfToken 做比較,如果不同的話,就拋出異常。

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

      3.LazyCsrfTokenRepository

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

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

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

      1

      2

      3

      4

      生成 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; } } } } }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      36

      37

      38

      39

      40

      41

      42

      43

      44

      45

      46

      47

      48

      49

      要學就學透徹!Spring Security 中 CSRF 防御源碼解析

      50

      51

      52

      53

      54

      55

      56

      57

      58

      這里,我說三點:

      generateToken 方法,該方法用來生成 CsrfToken,默認 CsrfToken 的載體是 DefaultCsrfToken,現在換成了 SaveOnAccessCsrfToken。

      SaveOnAccessCsrfToken 和 DefaultCsrfToken 并沒有太大區別,主要是 getToken 方法有區別,在 SaveOnAccessCsrfToken 中,當開發者調用 getToken 想要去獲取 csrfToken 時,才會去對 csrfToken 做保存操作(調用 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的 saveToken 方法)。

      LazyCsrfTokenRepository 自己的 saveToken 則做了修改,相當于放棄了 saveToken 的功能,調用該方法并不會做保存操作。

      使用了 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())); }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      4.小結

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

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

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

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

      好啦,不知道小伙伴們有沒有 GET 到呢?如果覺得有收獲,記得點個在看鼓勵下松哥哦~

      Spring

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:Excel是如何讓多人同時編輯
      下一篇:Excel2013用紅燈圖標記錄的方法
      相關文章
      亚洲精品国产精品乱码不卡| 亚洲人成图片网站| 亚洲av乱码中文一区二区三区| 亚洲大尺码专区影院| 亚洲成人福利在线| 亚洲美女免费视频| 亚洲首页在线观看| 亚洲综合激情六月婷婷在线观看| 亚洲免费在线播放| 亚洲精品动漫在线| 亚洲欧洲精品国产区| 亚洲国产美女视频| 亚洲国产亚洲片在线观看播放| 亚洲国产视频一区| 亚洲欧洲日韩国产一区二区三区 | 色偷偷女男人的天堂亚洲网| 亚洲噜噜噜噜噜影院在线播放| 亚洲嫩草影院在线观看| 亚洲免费在线观看视频| 2020亚洲男人天堂精品| 亚洲欧美日韩中文二区| 亚洲AV成人无码网天堂| 亚洲欧洲精品成人久久奇米网| 亚洲片国产一区一级在线观看| 亚洲午夜久久久久久噜噜噜| 亚洲免费观看视频| 亚洲AV成人片色在线观看| 99亚洲精品高清一二区| 亚洲免费在线视频播放| 亚洲色偷偷综合亚洲av78| 国产精品久久久久久亚洲影视| 亚洲成?v人片天堂网无码| 久久激情亚洲精品无码?V| 久久国产亚洲精品麻豆| 亚洲国产美国国产综合一区二区 | 亚洲午夜精品第一区二区8050| 亚洲中文字幕久久精品无码喷水| 亚洲国产成人一区二区精品区| 亚洲黄色网址在线观看| 麻豆狠色伊人亚洲综合网站| 大桥未久亚洲无av码在线 |