电脑知识|欧美黑人一区二区三区|软件|欧美黑人一级爽快片淫片高清|系统|欧美黑人狂野猛交老妇|数据库|服务器|编程开发|网络运营|知识问答|技术教程文章 - 好吧啦网

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

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

瀏覽:4日期: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
相關文章:
主站蜘蛛池模板: 小型手持气象站-空气负氧离子监测站-多要素微气象传感器-山东天合环境科技有限公司 | 铝单板_铝窗花_铝单板厂家_氟碳包柱铝单板批发价格-佛山科阳金属 | 即用型透析袋,透析袋夹子,药敏纸片,L型涂布棒-上海桥星贸易有限公司 | 四探针电阻率测试仪-振实密度仪-粉末流动性测定仪-宁波瑞柯微智能 | 炭黑吸油计_测试仪,单颗粒子硬度仪_ASTM标准炭黑自销-上海贺纳斯仪器仪表有限公司(HITEC中国办事处) | 药品冷藏箱厂家_低温冰箱_洁净工作台-济南欧莱博电子商务有限公司官网 | 神超官网_焊接圆锯片_高速钢锯片_硬质合金锯片_浙江神超锯业制造有限公司 | 冲锋衣滑雪服厂家-冲锋衣定制工厂-滑雪服加工厂-广东睿牛户外(S-GERT) | 跨境物流_美国卡派_中大件运输_尾程派送_海外仓一件代发 - 广州环至美供应链平台 | 雷冲击高压发生器-水内冷直流高压发生器-串联谐振分压器-武汉特高压电力科技有限公司 | 水稻烘干机,小麦烘干机,大豆烘干机,玉米烘干机,粮食烘干机_巩义市锦华粮食烘干机械制造有限公司 水环真空泵厂家,2bv真空泵,2be真空泵-淄博真空设备厂 | 东莞画册设计_logo/vi设计_品牌包装设计 - 华略品牌设计公司 | 河南生物显微镜,全自动冰冻切片机-河南荣程联合科技有限公司 | 强效碱性清洗剂-实验室中性清洗剂-食品级高纯氮气发生器-上海润榕科学器材有限公司 | 中空玻璃生产线,玻璃加工设备,全自动封胶线,铝条折弯机,双组份打胶机,丁基胶/卧式/立式全自动涂布机,玻璃设备-山东昌盛数控设备有限公司 | 跨境物流_美国卡派_中大件运输_尾程派送_海外仓一件代发 - 广州环至美供应链平台 | 电磁流量计厂家_涡街流量计厂家_热式气体流量计-青天伟业仪器仪表有限公司 | 螺旋压榨机-刮泥机-潜水搅拌机-电动泥斗-潜水推流器-南京格林兰环保设备有限公司 | 代做标书-代写标书-专业标书文件编辑-「深圳卓越创兴公司」 | 气动调节阀,电动调节阀,自力式压力调节阀,切断阀「厂家」-浙江利沃夫自控阀门 | 煤机配件厂家_刮板机配件_链轮轴组_河南双志机械设备有限公司 | 谈股票-今日股票行情走势分析-牛股推荐排行榜 | 电磁铁_小型推拉电磁铁_电磁阀厂家-深圳市宗泰电机有限公司 | 扒渣机厂家_扒渣机价格_矿用扒渣机_铣挖机_撬毛台车_襄阳永力通扒渣机公司 | 档案密集柜_手动密集柜_智能密集柜_内蒙古档案密集柜-盛隆柜业内蒙古密集柜直销中心 | 酒万铺-酒水招商-酒水代理| 泰来华顿液氮罐,美国MVE液氮罐,自增压液氮罐,定制液氮生物容器,进口杜瓦瓶-上海京灿精密机械有限公司 | 皮带机_移动皮带机_大倾角皮带机_皮带机厂家 - 新乡市国盛机械设备有限公司 | 建筑资质代办-建筑企业资质代办机构-建筑资质代办公司 | 广州办公室设计,办公室装修,写字楼设计,办公室装修公司_德科 | 深圳侦探联系方式_深圳小三调查取证公司_深圳小三分离机构 | 直流电能表-充电桩电能表-导轨式电能表-智能电能表-浙江科为电气有限公司 | 粉末冶金-粉末冶金齿轮-粉末冶金零件厂家-东莞市正朗精密金属零件有限公司 | 退火炉,燃气退火炉,燃气热处理炉生产厂家-丹阳市丰泰工业炉有限公司 | 无锡装修装潢公司,口碑好的装饰装修公司-无锡索美装饰设计工程有限公司 | 南京种植牙医院【官方挂号】_南京治疗种植牙医院那个好_南京看种植牙哪里好_南京茀莱堡口腔医院 尼龙PA610树脂,尼龙PA612树脂,尼龙PA1010树脂,透明尼龙-谷骐科技【官网】 | 全自动翻转振荡器-浸出式水平振荡器厂家-土壤干燥箱价格-常州普天仪器 | 防伪溯源|防窜货|微信二维码营销|兆信_行业内领先的防伪防窜货数字化营销解决方案供应商 | 顺景erp系统_erp软件_erp软件系统_企业erp管理系统-广东顺景软件科技有限公司 | 过滤器_自清洗过滤器_气体过滤器_苏州华凯过滤技术有限公司 | IWIS链条代理-ALPS耦合透镜-硅烷预处理剂-上海顶楚电子有限公司 lcd条形屏-液晶长条屏-户外广告屏-条形智能显示屏-深圳市条形智能电子有限公司 |