Spring Security 兩種資源放行策略,千萬別用錯了!

事情的原由是這樣,有小夥伴在微信上問了鬆哥一個問題:css

就是他使用 Spring Security 作用戶登陸,等成功後,結果沒法獲取到登陸用戶信息,鬆哥以前寫過相關的文章(奇怪,Spring Security 登陸成功後老是獲取不到登陸用戶信息?),可是他彷佛沒有看懂。考慮到這是一個很是常見的問題,所以我想今天換個角度再來和大夥聊一聊這個話題。html

Spring Security 中,到底該怎麼樣給資源額外放行?前端

1.兩種思路

在 Spring Security 中,有一個資源,若是你但願用戶不用登陸就能訪問,那麼通常來講,你有兩種配置策略:java

第一種就是在 configure(WebSecurity web) 方法中配置放行,像下面這樣:web

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/css/**", "/js/**", "/index.html", "/img/**", "/fonts/**", "/favicon.ico", "/verifyCode");
}

第二種方式是在 configure(HttpSecurity http) 方法中進行配置:spring

http.authorizeRequests()
        .antMatchers("/hello").permitAll()
        .anyRequest().authenticated()

兩種方式最大的區別在於,第一種方式是不走 Spring Security 過濾器鏈,而第二種方式走 Spring Security 過濾器鏈,在過濾器鏈中,給請求放行。後端

在咱們使用 Spring Security 的時候,有的資源可使用第一種方式額外放行,不須要驗證,例如前端頁面的靜態資源,就能夠按照第一種方式配置放行。微信

有的資源放行,則必須使用第二種方式,例如登陸接口。你們知道,登陸接口也是必需要暴露出來的,不須要登陸就能訪問到的,可是咱們卻不能將登陸接口用第一種方式暴露出來,登陸請求必需要走 Spring Security 過濾器鏈,由於在這個過程當中,還有其餘事情要作。session

接下來我以登陸接口爲例,來和小夥伴們分析一下走 Spring Security 過濾器鏈有什麼不一樣。ide

2.登陸請求分析

首先你們知道,當咱們使用 Spring Security,用戶登陸成功以後,有兩種方式獲取用戶登陸信息:

  1. SecurityContextHolder.getContext().getAuthentication()
  2. 在 Controller 的方法中,加入 Authentication 參數

這兩種辦法,均可以獲取到當前登陸用戶信息。具體的操做辦法,你們能夠看看鬆哥以前發佈的教程:Spring Security 如何動態更新已登陸用戶信息?

這兩種方式獲取到的數據都是來自 SecurityContextHolder,SecurityContextHolder 中的數據,本質上是保存在 ThreadLocal 中,ThreadLocal 的特色是存在它裏邊的數據,哪一個線程存的,哪一個線程才能訪問到。

這樣就帶來一個問題,當用戶登陸成功以後,將用戶用戶數據存在 SecurityContextHolder 中(thread1),當下一個請求來的時候(thread2),想從 SecurityContextHolder 中獲取用戶登陸信息,卻發現獲取不到!爲啥?由於它倆不是同一個 Thread。

但實際上,正常狀況下,咱們使用 Spring Security 登陸成功後,之後每次都可以獲取到登陸用戶信息,這又是怎麼回事呢?

這咱們就要引入 Spring Security 中的 SecurityContextPersistenceFilter 了。

小夥伴們都知道,不管是 Spring Security 仍是 Shiro,它的一系列功能其實都是由過濾器來完成的,在 Spring Security 中,鬆哥前面跟你們聊了 UsernamePasswordAuthenticationFilter 過濾器,在這個過濾器以前,還有一個過濾器就是 SecurityContextPersistenceFilter,請求在到達 UsernamePasswordAuthenticationFilter 以前都會先通過 SecurityContextPersistenceFilter

咱們來看下它的源碼(部分):

public class SecurityContextPersistenceFilter extends GenericFilterBean {
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
                response);
        SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
        try {
            SecurityContextHolder.setContext(contextBeforeChainExecution);
            chain.doFilter(holder.getRequest(), holder.getResponse());
        }
        finally {
            SecurityContext contextAfterChainExecution = SecurityContextHolder
                    .getContext();
            SecurityContextHolder.clearContext();
            repo.saveContext(contextAfterChainExecution, holder.getRequest(),
                    holder.getResponse());
        }
    }
}

本來的方法很長,我這裏列出來了比較關鍵的幾個部分:

  1. SecurityContextPersistenceFilter 繼承自 GenericFilterBean,而 GenericFilterBean 則是 Filter 的實現,因此 SecurityContextPersistenceFilter 做爲一個過濾器,它裏邊最重要的方法就是 doFilter 了。
  2. 在 doFilter 方法中,它首先會從 repo 中讀取一個 SecurityContext 出來,這裏的 repo 實際上就是 HttpSessionSecurityContextRepository,讀取 SecurityContext 的操做會進入到 readSecurityContextFromSession 方法中,在這裏咱們看到了讀取的核心方法 Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);,這裏的 springSecurityContextKey 對象的值就是 SPRING_SECURITY_CONTEXT,讀取出來的對象最終會被轉爲一個 SecurityContext 對象。
  3. SecurityContext 是一個接口,它有一個惟一的實現類 SecurityContextImpl,這個實現類其實就是用戶信息在 session 中保存的 value。
  4. 在拿到 SecurityContext 以後,經過 SecurityContextHolder.setContext 方法將這個 SecurityContext 設置到 ThreadLocal 中去,這樣,在當前請求中,Spring Security 的後續操做,咱們均可以直接從 SecurityContextHolder 中獲取到用戶信息了。
  5. 接下來,經過 chain.doFilter 讓請求繼續向下走(這個時候就會進入到 UsernamePasswordAuthenticationFilter 過濾器中了)。
  6. 在過濾器鏈走完以後,數據響應給前端以後,finally 中還有一步收尾操做,這一步很關鍵。這裏從 SecurityContextHolder 中獲取到 SecurityContext,獲取到以後,會把 SecurityContextHolder 清空,而後調用 repo.saveContext 方法將獲取到的 SecurityContext 存入 session 中。

至此,整個流程就很明瞭了。

每個請求到達服務端的時候,首先從 session 中找出來 SecurityContext ,而後設置到 SecurityContextHolder 中去,方便後續使用,當這個請求離開的時候,SecurityContextHolder 會被清空,SecurityContext 會被放回 session 中,方便下一個請求來的時候獲取。

登陸請求來的時候,尚未登陸用戶數據,可是登陸請求走的時候,會將用戶登陸數據存入 session 中,下個請求到來的時候,就能夠直接取出來用了。

看了上面的分析,咱們能夠至少得出兩點結論:

  1. 若是咱們暴露登陸接口的時候,使用了前面提到的第一種方式,沒有走 Spring Security,過濾器鏈,則在登陸成功後,就不會將登陸用戶信息存入 session 中,進而致使後來的請求都沒法獲取到登陸用戶信息(後來的請求在系統眼裏也都是未認證的請求)
  2. 若是你的登陸請求正常,走了 Spring Security 過濾器鏈,可是後來的 A 請求沒走過濾器鏈(採用前面提到的第一種方式放行),那麼 A 請求中,也是沒法經過 SecurityContextHolder 獲取到登陸用戶信息的,由於它一開始沒通過 SecurityContextPersistenceFilter 過濾器鏈。

3.小結

總之,前端靜態資源放行時,能夠直接不走 Spring Security 過濾器鏈,像下面這樣:

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/css/**","/js/**","/index.html","/img/**","/fonts/**","/favicon.ico");
}

後端的接口要額外放行,就須要仔細考慮場景了,不過通常來講,不建議使用上面這種方式,建議下面這種方式,緣由前面已經說過了:

http.authorizeRequests()
        .antMatchers("/hello").permitAll()
        .anyRequest().authenticated()

好了,這就是和小夥伴們分享的兩種資源放行策略,你們千萬別搞錯了哦~

有收穫的話,記得點個在看鼓勵下鬆哥哦~

相關文章
相關標籤/搜索