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

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

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

1.問題復(fù)現(xiàn)

如果使用了 Spring Security虫溜,當(dāng)我們登錄成功后雹姊,可以通過如下方式獲取到當(dāng)前登錄用戶信息:

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

這兩種辦法吼渡,都可以獲取到當(dāng)前登錄用戶信息容为。具體的操作辦法,大家可以看看松哥之前發(fā)布的教程:Spring Security 如何動態(tài)更新已登錄用戶信息寺酪?坎背。

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

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

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

  1. 無法獲取到當(dāng)前登錄用戶信息奸腺。
  2. 當(dāng)你發(fā)送任何請求,系統(tǒng)都會給你返回 401副砍。

2.順藤摸瓜

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

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

首先松哥之前和大家聊過,SecurityContextHolder 中的數(shù)據(jù),本質(zhì)上是保存在 ThreadLocal 中,ThreadLocal 的特點是存在它里邊的數(shù)據(jù),哪個線程存的呛梆,哪個線程才能訪問到霎终。

這樣就帶來一個問題,當(dāng)不同的請求進(jìn)入到服務(wù)端之后阅茶,由不同的 thread 去處理撞蜂,按理說后面的請求就可能無法獲取到登錄請求的線程存入的數(shù)據(jù)送漠,例如登錄請求在線程 A 中將登錄用戶信息存入 ThreadLocal爷狈,后面的請求來了羡微,在線程 B 中處理毅哗,那此時就無法獲取到用戶的登錄信息翅睛。

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

這我們就要引入 Spring Security 中的 SecurityContextPersistenceFilter 了冲呢。

小伙伴們都知道瓢颅,無論是 Spring Security 還是 Shiro,它的一系列功能其實都是由過濾器來完成的渔嚷,在 Spring Security 中量瓜,松哥前面跟大家聊了 UsernamePasswordAuthenticationFilter 過濾器,在這個過濾器之前,還有一個過濾器就是 SecurityContextPersistenceFilter,請求在到達(dá) UsernamePasswordAuthenticationFilter 之前都會先經(jīng)過 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());
        }
    }
}

原本的方法很長赠尾,我這里列出來了比較關(guān)鍵的幾個部分:

  1. SecurityContextPersistenceFilter 繼承自 GenericFilterBean甲棍,而 GenericFilterBean 則是 Filter 的實現(xiàn)唱遭,所以 SecurityContextPersistenceFilter 作為一個過濾器,它里邊最重要的方法就是 doFilter 了呈驶。
  2. 在 doFilter 方法中拷泽,它首先會從 repo 中讀取一個 SecurityContext 出來,這里的 repo 實際上就是 HttpSessionSecurityContextRepository袖瞻,讀取 SecurityContext 的操作會進(jìn)入到 readSecurityContextFromSession 方法中司致,在這里我們看到了讀取的核心方法 Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);,這里的 springSecurityContextKey 對象的值就是 SPRING_SECURITY_CONTEXT聋迎,讀取出來的對象最終會被轉(zhuǎn)為一個 SecurityContext 對象脂矫。
  3. SecurityContext 是一個接口,它有一個唯一的實現(xiàn)類 SecurityContextImpl霉晕,這個實現(xiàn)類其實就是用戶信息在 session 中保存的 value庭再。
  4. 在拿到 SecurityContext 之后捞奕,通過 SecurityContextHolder.setContext 方法將這個 SecurityContext 設(shè)置到 ThreadLocal 中去,這樣拄轻,在當(dāng)前請求中颅围,Spring Security 的后續(xù)操作,我們都可以直接從 SecurityContextHolder 中獲取到用戶信息了恨搓。
  5. 接下來院促,通過 chain.doFilter 讓請求繼續(xù)向下走(這個時候就會進(jìn)入到 UsernamePasswordAuthenticationFilter 過濾器中了)。
  6. 在過濾器鏈走完之后斧抱,數(shù)據(jù)響應(yīng)給前端之后常拓,finally 中還有一步收尾操作,這一步很關(guān)鍵辉浦。這里從 SecurityContextHolder 中獲取到 SecurityContext墩邀,獲取到之后,會把 SecurityContextHolder 清空盏浙,然后調(diào)用 repo.saveContext 方法將獲取到的 SecurityContext 存入 session 中。

至此荔茬,整個流程就很明了了废膘。

每一個請求到達(dá)服務(wù)端的時候,首先從 session 中找出來 SecurityContext 慕蔚,然后設(shè)置到 SecurityContextHolder 中去丐黄,方便后續(xù)使用,當(dāng)這個請求離開的時候孔飒,SecurityContextHolder 會被清空灌闺,SecurityContext 會被放回 session 中,方便下一個請求來的時候獲取坏瞄。

搞明白這一點之后桂对,再去解決 Spring Security 登錄后無法獲取到當(dāng)前登錄用戶這個問題,就非常 easy 了鸠匀。

3.問題解決

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

最簡單情況的就是你在一個新的線程中去執(zhí)行 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 中加載到用戶信息父腕,進(jìn)而導(dǎo)致 SecurityContextHolder 里邊空空如也。

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

  • 上一個請求臨走的時候萧诫,沒有將數(shù)據(jù)存儲到 session 中去。
  • 當(dāng)前請求自己沒走過濾器鏈杜顺。

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

當(dāng)我們想讓 Spring Security 中的資源可以匿名訪問時尖奔,我們有兩種辦法:

  1. 不走 Spring Security 過濾器鏈。
  2. 繼續(xù)走 Spring Security 過濾器鏈穷当,但是可以匿名訪問提茁。

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

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

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

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

如果你很不巧,把登錄請求地址放進(jìn)來了智嚷,那就 gg 了卖丸。雖然登錄請求可以被所有人訪問,但是不能放在這里(而應(yīng)該通過允許匿名訪問的方式來給請求放行)盏道。如果放在這里稍浆,登錄請求將不走 SecurityContextPersistenceFilter 過濾器,也就意味著不會將登錄用戶信息存入 session猜嘱,進(jìn)而導(dǎo)致后續(xù)請求無法獲取到登錄用戶信息衅枫。

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

好了朗伶,小伙伴們?nèi)绻谑褂?Spring Security 時遇到類似問題弦撩,不妨按照本文提供的思路來解決一下。如果覺得有收獲论皆,記得點一下右下角在看哦

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末孤钦,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子纯丸,更是在濱河造成了極大的恐慌偏形,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件觉鼻,死亡現(xiàn)場離奇詭異俊扭,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)坠陈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門萨惑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捐康,“玉大人,你說我怎么就攤上這事庸蔼〗庾埽” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵姐仅,是天一觀的道長花枫。 經(jīng)常有香客問我,道長掏膏,這世上最難降的妖魔是什么劳翰? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮馒疹,結(jié)果婚禮上佳簸,老公的妹妹穿的比我還像新娘。我一直安慰自己颖变,他們只是感情好生均,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著腥刹,像睡著了一般疯特。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肛走,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音录别,去河邊找鬼朽色。 笑死,一個胖子當(dāng)著我的面吹牛组题,可吹牛的內(nèi)容都是我干的葫男。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼崔列,長吁一口氣:“原來是場噩夢啊……” “哼梢褐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赵讯,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤盈咳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后边翼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鱼响,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年组底,在試婚紗的時候發(fā)現(xiàn)自己被綠了丈积。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片筐骇。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖江滨,靈堂內(nèi)的尸體忽然破棺而出铛纬,到底是詐尸還是另有隱情,我是刑警寧澤唬滑,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布告唆,位于F島的核電站,受9級特大地震影響间雀,放射性物質(zhì)發(fā)生泄漏悔详。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一惹挟、第九天 我趴在偏房一處隱蔽的房頂上張望茄螃。 院中可真熱鬧,春花似錦连锯、人聲如沸归苍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拼弃。三九已至,卻和暖如春摇展,著一層夾襖步出監(jiān)牢的瞬間吻氧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工咏连, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留盯孙,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓祟滴,卻偏偏與公主長得像振惰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子垄懂,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容