奇怪,Spring Security 登錄成功后總是獲取不到登錄用戶信息?

有好幾位小伙伴小伙伴曾向松哥求助過這個問題。

一開始我覺得這可能是一個小概率 BUG,但是當問的人多了,我覺得這個問題對于新手來說還有一定的普遍性,有必要來寫篇文章跟大家仔細聊一聊這個問題,防止小伙伴們掉坑。

1.問題復現

如果使用了 Spring Security,當我們登錄成功后,可以通過如下方式獲取到當前登錄用戶信息:

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

這兩種辦法,都可以獲取到當前登錄用戶信息。具體的操作辦法,大家可以看看松哥之前發布的教程:Spring Security 如何動態更新已登錄用戶信息?

正常情況下,我們通過如上兩種方式的任意一種就可以獲取到已經登錄的用戶信息。

異常情況,就是這兩種方式中的任意一種,都返回 null。

都返回 null,意味著系統收到當前請求時并不知道你已經登錄了(因為你沒有在系統中留下任何有效信息),這會帶來兩個問題:

  1. 無法獲取到當前登錄用戶信息。
  2. 當你發送任何請求,系統都會給你返回 401。

2.順藤摸瓜

要弄明白這個問題,我們就得明白 Spring Security 中的用戶信息到底是在哪里存的?

前面說了兩種數據獲取方式,但是這兩種數據獲取方式,獲取到的數據又是從哪里來的?

首先松哥之前和大家聊過,SecurityContextHolder 中的數據,本質上是保存在 ThreadLocal 中,ThreadLocal 的特點是存在它里邊的數據,哪個線程存的,哪個線程才能訪問到。

這樣就帶來一個問題,當不同的請求進入到服務端之后,由不同的 thread 去處理,按理說后面的請求就可能無法獲取到登錄請求的線程存入的數據,例如登錄請求在線程 A 中將登錄用戶信息存入 ThreadLocal,后面的請求來了,在線程 B 中處理,那此時就無法獲取到用戶的登錄信息。

但實際上,正常情況下,我們每次都能夠獲取到登錄用戶信息,這又是怎么回事呢?

這我們就要引入 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 中,方便下一個請求來的時候獲取。

搞明白這一點之后,再去解決 Spring Security 登錄后無法獲取到當前登錄用戶這個問題,就非常 easy 了。

3.問題解決

經過上面的分析之后,我們再來回顧一下為什么會發生登錄之后無法獲取到當前用戶信息這樣的事情?

最簡單情況的就是你在一個新的線程中去執行 SecurityContextHolder.getContext().getAuthentication(),這肯定獲取不到用戶信息,無需多說。例如下面這樣:

@GetMapping("/menu")
public List<Menu> getMenusByHrId() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            System.out.println(authentication);
        }
    }).start();
    return menuService.getMenusByHrId();
}

這種簡單的問題相信大家都能夠很容易排查到。

還有一種隱藏比較深的就是在 SecurityContextPersistenceFilter 的 doFilter 方法中沒能從 session 中加載到用戶信息,進而導致 SecurityContextHolder 里邊空空如也。

在 SecurityContextPersistenceFilter 中沒能加載到用戶信息,原因可能就比較多了,例如:

  • 上一個請求臨走的時候,沒有將數據存儲到 session 中去。
  • 當前請求自己沒走過濾器鏈。

什么時候會發生這個問題呢?有的小伙伴可能在配置 SecurityConfig#configure(WebSecurity) 方法時,會忽略掉一個重要的點。

當我們想讓 Spring Security 中的資源可以匿名訪問時,我們有兩種辦法:

  1. 不走 Spring Security 過濾器鏈。
  2. 繼續走 Spring Security 過濾器鏈,但是可以匿名訪問。

這兩種辦法對應了兩種不同的配置方式。其中第一種配置可能會影響到我們獲取登錄用戶信息,第二種則不影響,所以這里我們來重點看看第一種。

不想走 Spring Security 過濾器鏈,我們一般可以通過如下方式配置:

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

正常這樣配置是沒有問題的。

如果你很不巧,把登錄請求地址放進來了,那就 gg 了。雖然登錄請求可以被所有人訪問,但是不能放在這里(而應該通過允許匿名訪問的方式來給請求放行)。如果放在這里,登錄請求將不走 SecurityContextPersistenceFilter 過濾器,也就意味著不會將登錄用戶信息存入 session,進而導致后續請求無法獲取到登錄用戶信息。

這也就是一開始小伙伴遇到的問題。

好了,小伙伴們如果在使用 Spring Security 時遇到類似問題,不妨按照本文提供的思路來解決一下。如果覺得有收獲,記得點一下右下角在看哦

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內容