亚洲精品久久久中文字幕-亚洲精品久久片久久-亚洲精品久久青草-亚洲精品久久婷婷爱久久婷婷-亚洲精品久久午夜香蕉

您的位置:首頁技術文章
文章詳情頁

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

瀏覽:2日期:2023-01-22 14:26:37

上篇文章中,我們講了在 Spring Security 中如何踢掉前一個登錄用戶,或者禁止用戶二次登錄,通過一個簡單的案例,實現了我們想要的效果。

但是有一個不太完美的地方,就是我們的用戶是配置在內存中的用戶,我們沒有將用戶放到數據庫中去。正常情況下,松哥在 Spring Security 系列中講的其他配置,大家只需要參考Spring Security+Spring Data Jpa 強強聯手,安全管理只有更簡單!一文,將數據切換為數據庫中的數據即可。

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

挖一個大坑,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 的 session 并發處理時,直接將內存中的用戶切換為數據庫中的用戶會有問題,今天我們就來說說這個問題,順便把這個功能應用到微人事中(https://github.com/lenve/vhr )。

本文的案例將基于Spring Security+Spring Data Jpa 強強聯手,安全管理只有更簡單!一文來構建,所以重復的代碼我就不寫了,小伙伴們要是不熟悉可以參考該篇文章。

1.環境準備

首先,我們打開Spring Security+Spring Data Jpa 強強聯手,安全管理只有更簡單!一文中的案例,這個案例結合 Spring Data Jpa 將用戶數據存儲到數據庫中去了。

然后我們將上篇文章中涉及到的登錄頁面拷貝到項目中(文末可以下載完整案例):

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-7XB0viq6-1588898082940)(http://img.itboyhub.com/2020/...]

并在 SecurityConfig 中對登錄頁面稍作配置:

@Overridepublic void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers('/js/**', '/css/**', '/images/**');}@Overrideprotected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() ... .and() .formLogin() .loginPage('/login.html') .loginProcessingUrl('/doLogin') ... .and() .sessionManagement() .maximumSessions(1);}

這里都是常規配置,我就不再多說。注意最后面我們將 session 數量設置為 1。

好了,配置完成后,我們啟動項目,并行性多端登錄測試。

打開多個瀏覽器,分別進行多端登錄測試,我們驚訝的發現,每個瀏覽器都能登錄成功,每次登錄成功也不會踢掉已經登錄的用戶!

這是怎么回事?

2.問題分析

要搞清楚這個問題,我們就要先搞明白 Spring Security 是怎么保存用戶對象和 session 的。

Spring Security 中通過 SessionRegistryImpl 類來實現對會話信息的統一管理,我們來看下這個類的源碼(部分):

public class SessionRegistryImpl implements SessionRegistry, ApplicationListener<SessionDestroyedEvent> { /** <principal:Object,SessionIdSet> */ private final ConcurrentMap<Object, Set<String>> principals; /** <sessionId:Object,SessionInformation> */ private final Map<String, SessionInformation> sessionIds; public void registerNewSession(String sessionId, Object principal) { if (getSessionInformation(sessionId) != null) { removeSessionInformation(sessionId); } sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date())); principals.compute(principal, (key, sessionsUsedByPrincipal) -> { if (sessionsUsedByPrincipal == null) { sessionsUsedByPrincipal = new CopyOnWriteArraySet<>(); } sessionsUsedByPrincipal.add(sessionId); return sessionsUsedByPrincipal; }); } public void removeSessionInformation(String sessionId) { SessionInformation info = getSessionInformation(sessionId); if (info == null) { return; } sessionIds.remove(sessionId); principals.computeIfPresent(info.getPrincipal(), (key, sessionsUsedByPrincipal) -> { sessionsUsedByPrincipal.remove(sessionId); if (sessionsUsedByPrincipal.isEmpty()) { sessionsUsedByPrincipal = null; } return sessionsUsedByPrincipal; }); }}

這個類的源碼還是比較長,我這里提取出來一些比較關鍵的部分:

首先大家看到,一上來聲明了一個 principals 對象,這是一個支持并發訪問的 map 集合,集合的 key 就是用戶的主體(principal),正常來說,用戶的 principal 其實就是用戶對象,松哥在之前的文章中也和大家講過 principal 是怎么樣存入到 Authentication 中的(參見: Spring Security 登錄流程),而集合的 value 則是一個 set 集合,這個 set 集合中保存了這個用戶對應的 sessionid。 如有新的 session 需要添加,就在 registerNewSession 方法中進行添加,具體是調用 principals.compute 方法進行添加,key 就是 principal。 如果用戶注銷登錄,sessionid 需要移除,相關操作在 removeSessionInformation 方法中完成,具體也是調用 principals.computeIfPresent 方法,這些關于集合的基本操作我就不再贅述了。

看到這里,大家發現一個問題,ConcurrentMap 集合的 key 是 principal 對象,用對象做 key,一定要重寫 equals 方法和 hashCode 方法,否則第一次存完數據,下次就找不到了,這是 JavaSE 方面的知識,我就不用多說了。

如果我們使用了基于內存的用戶,我們來看下 Spring Security 中的定義:

public class User implements UserDetails, CredentialsContainer { private String password; private final String username; private final Set<GrantedAuthority> authorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled; @Override public boolean equals(Object rhs) { if (rhs instanceof User) { return username.equals(((User) rhs).username); } return false; } @Override public int hashCode() { return username.hashCode(); }}

可以看到,他自己實際上是重寫了 equals 和 hashCode 方法了。

所以我們使用基于內存的用戶時沒有問題,而我們使用自定義的用戶就有問題了。

找到了問題所在,那么解決問題就很容易了,重寫 User 類的 equals 方法和 hashCode 方法即可:

@Entity(name = 't_user')public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; private boolean accountNonExpired; private boolean accountNonLocked; private boolean credentialsNonExpired; private boolean enabled; @ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.PERSIST) private List<Role> roles; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return Objects.equals(username, user.username); } @Override public int hashCode() { return Objects.hash(username); } ... ...}

配置完成后,重啟項目,再去進行多端登錄測試,發現就可以成功踢掉已經登錄的用戶了。

如果你使用了 MyBatis 而不是 Jpa,也是一樣的處理方案,只需要重寫登錄用戶的 equals 方法和 hashCode 方法即可。

3.微人事應用

3.1 存在的問題

由于微人事目前是采用了 JSON 格式登錄,所以如果項目控制 session 并發數,就會有一些額外的問題要處理。

最大的問題在于我們用自定義的過濾器代替了 UsernamePasswordAuthenticationFilter,進而導致前面所講的關于 session 的配置,統統失效。所有相關的配置我們都要在新的過濾器 LoginFilter 中進行配置 ,包括 SessionAuthenticationStrategy 也需要我們自己手動配置了。

這雖然帶來了一些工作量,但是做完之后,相信大家對于 Spring Security 的理解又會更上一層樓。

3.2 具體應用

我們來看下具體怎么實現,我這里主要列出來一些關鍵代碼,完整代碼大家可以從 GitHub 上下載:https://github.com/lenve/vhr 。

首先第一步,我們重寫 Hr 類的 equals 和 hashCode 方法,如下:

public class Hr implements UserDetails { ... ... @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Hr hr = (Hr) o; return Objects.equals(username, hr.username); } @Override public int hashCode() { return Objects.hash(username); } ... ...}

接下來在 SecurityConfig 中進行配置。

這里我們要自己提供 SessionAuthenticationStrategy,而前面處理 session 并發的是 ConcurrentSessionControlAuthenticationStrategy,也就是說,我們需要自己提供一個 ConcurrentSessionControlAuthenticationStrategy 的實例,然后配置給 LoginFilter,但是在創建 ConcurrentSessionControlAuthenticationStrategy 實例的過程中,還需要有一個 SessionRegistryImpl 對象。

前面我們說過,SessionRegistryImpl 對象是用來維護會話信息的,現在這個東西也要我們自己來提供,SessionRegistryImpl 實例很好創建,如下:

@BeanSessionRegistryImpl sessionRegistry() { return new SessionRegistryImpl();}

然后在 LoginFilter 中配置 SessionAuthenticationStrategy,如下:

@BeanLoginFilter loginFilter() throws Exception { LoginFilter loginFilter = new LoginFilter(); loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> { //省略 } ); loginFilter.setAuthenticationFailureHandler((request, response, exception) -> { //省略 } ); loginFilter.setAuthenticationManager(authenticationManagerBean()); loginFilter.setFilterProcessesUrl('/doLogin'); ConcurrentSessionControlAuthenticationStrategy sessionStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry()); sessionStrategy.setMaximumSessions(1); loginFilter.setSessionAuthenticationStrategy(sessionStrategy); return loginFilter;}

我們在這里自己手動構建 ConcurrentSessionControlAuthenticationStrategy 實例,構建時傳遞 SessionRegistryImpl 參數,然后設置 session 的并發數為 1,最后再將 sessionStrategy 配置給 LoginFilter。

其實上篇文章中,我們的配置方案,最終也是像上面這樣,只不過現在我們自己把這個寫出來了而已。

這就配置完了嗎?沒有!session 處理還有一個關鍵的過濾器叫做 ConcurrentSessionFilter,本來這個過濾器是不需要我們管的,但是這個過濾器中也用到了 SessionRegistryImpl,而 SessionRegistryImpl 現在是由我們自己來定義的,所以,該過濾器我們也要重新配置一下,如下:

@Overrideprotected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() ... http.addFilterAt(new ConcurrentSessionFilter(sessionRegistry(), event -> { HttpServletResponse resp = event.getResponse(); resp.setContentType('application/json;charset=utf-8'); resp.setStatus(401); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString(RespBean.error('您已在另一臺設備登錄,本次登錄已下線!'))); out.flush(); out.close(); }), ConcurrentSessionFilter.class); http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);}

在這里,我們重新創建一個 ConcurrentSessionFilter 的實例,代替系統默認的即可。在創建新的 ConcurrentSessionFilter 實例時,需要兩個參數:

sessionRegistry 就是我們前面提供的 SessionRegistryImpl 實例。 第二個參數,是一個處理 session 過期后的回調函數,也就是說,當用戶被另外一個登錄踢下線之后,你要給什么樣的下線提示,就在這里來完成。

最后,我們還需要在處理完登錄數據之后,手動向 SessionRegistryImpl 中添加一條記錄:

public class LoginFilter extends UsernamePasswordAuthenticationFilter { @Autowired SessionRegistry sessionRegistry; @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //省略 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); setDetails(request, authRequest); Hr principal = new Hr(); principal.setUsername(username); sessionRegistry.registerNewSession(request.getSession(true).getId(), principal); return this.getAuthenticationManager().authenticate(authRequest); } ... ... }}

在這里,我們手動調用 sessionRegistry.registerNewSession 方法,向 SessionRegistryImpl 中添加一條 session 記錄。

OK,如此之后,我們的項目就配置完成了。

接下來,重啟 vhr 項目,進行多端登錄測試,如果自己被人踢下線了,就會看到如下提示:

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

完整的代碼,我已經更新到 vhr 上了,大家可以下載學習。

4.小結

好了,本文主要和小伙伴們介紹了一個在 Spring Security 中處理 session 并發問題時,可能遇到的一個坑,以及在前后端分離情況下,如何處理 session 并發問題。不知道小伙伴們有沒有 GET 到呢?

本文第二小節的案例大家可以從 GitHub 上下載:https://github.com/lenve/spring-security-samples

如果覺得有收獲,記得點個在看鼓勵下松哥哦~

標簽: Spring
相關文章:
主站蜘蛛池模板: 一级特色黄色片 | 黄色一级片在线看 | 亚洲欧美在线视频 | 亚洲jjzzjjzz在线播放 | 爱爱欧美| 热99精品只有里视频最新 | 国产精品视频1区 | 黄色a免费 | 亚洲一区二区三 | 精品一二 | 手机看片高清国产日韩片 | 亚洲美女免费视频 | 中国内地毛片免费高清 | 国产乱子精品免费视观看片 | 一级特黄 | 亚洲美女色视频 | 久久久免费的精品 | 亚洲日韩中文字幕 | 91麻豆国产在线观看 | 亚洲午夜一级毛片 | 麻豆黄色| 日韩欧美综合视频 | 日韩免费精品一级毛片 | 在线一级视频 | 91天天操| 成人午夜影视全部免费看 | 青青热久免费精品视频在线观看 | 俄国特级毛片www免 俄罗斯14一18处交 | 尤物视频网在线观看 | 成年女人a毛片免费视频 | 欧美黄色大片视频 | 久久久亚洲国产精品主播 | 欧美色图亚洲自拍 | 国产成人精品免费视频大 | 亚洲一色| 亚洲国产日韩无在线播放 | 欧美性猛交xxxx乱大交蜜桃 | 正在播放亚洲一区 | 国产综合久久久久久 | xxxxx做受大片视频免费 | 国产视频每日更新 |