事情的原由是這樣,有小夥伴在微信上問了鬆哥一個問題:css
就是他使用 Spring Security 作用戶登陸,等成功後,結果沒法獲取到登陸用戶信息,鬆哥以前寫過相關的文章(奇怪,Spring Security 登陸成功後老是獲取不到登陸用戶信息?),可是他彷佛沒有看懂。考慮到這是一個很是常見的問題,所以我想今天換個角度再來和大夥聊一聊這個話題。html
Spring Security 中,到底該怎麼樣給資源額外放行?前端
在 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
首先你們知道,當咱們使用 Spring Security,用戶登陸成功以後,有兩種方式獲取用戶登陸信息:
SecurityContextHolder.getContext().getAuthentication()
這兩種辦法,均可以獲取到當前登陸用戶信息。具體的操做辦法,你們能夠看看鬆哥以前發佈的教程: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()); } } }
本來的方法很長,我這裏列出來了比較關鍵的幾個部分:
Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
,這裏的 springSecurityContextKey 對象的值就是 SPRING_SECURITY_CONTEXT,讀取出來的對象最終會被轉爲一個 SecurityContext 對象。UsernamePasswordAuthenticationFilter
過濾器中了)。至此,整個流程就很明瞭了。
每個請求到達服務端的時候,首先從 session 中找出來 SecurityContext ,而後設置到 SecurityContextHolder 中去,方便後續使用,當這個請求離開的時候,SecurityContextHolder 會被清空,SecurityContext 會被放回 session 中,方便下一個請求來的時候獲取。
登陸請求來的時候,尚未登陸用戶數據,可是登陸請求走的時候,會將用戶登陸數據存入 session 中,下個請求到來的時候,就能夠直接取出來用了。
看了上面的分析,咱們能夠至少得出兩點結論:
總之,前端靜態資源放行時,能夠直接不走 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()
好了,這就是和小夥伴們分享的兩種資源放行策略,你們千萬別搞錯了哦~
有收穫的話,記得點個在看鼓勵下鬆哥哦~