最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

 更新時(shí)間:2020年05月08日 10:33:48   作者:江南一點(diǎn)雨  
這篇文章主要介紹了Spring Boot + Vue 前后端分離項(xiàng)目如何踢掉已登錄用戶,需要的朋友可以參考下

上篇文章中,我們講了在 Spring Security 中如何踢掉前一個(gè)登錄用戶,或者禁止用戶二次登錄,通過(guò)一個(gè)簡(jiǎn)單的案例,實(shí)現(xiàn)了我們想要的效果。

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

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

  1. 挖一個(gè)大坑,Spring Security 開搞!
  2. 松哥手把手帶你入門 Spring Security,別再問(wèn)密碼怎么解密了
  3. 手把手教你定制 Spring Security 中的表單登錄
  4. Spring Security 做前后端分離,咱就別做頁(yè)面跳轉(zhuǎn)了!統(tǒng)統(tǒng) JSON 交互
  5. Spring Security 中的授權(quán)操作原來(lái)這么簡(jiǎn)單
  6. Spring Security 如何將用戶數(shù)據(jù)存入數(shù)據(jù)庫(kù)?
  7. Spring Security+Spring Data Jpa 強(qiáng)強(qiáng)聯(lián)手,安全管理只有更簡(jiǎn)單!
  8. Spring Boot + Spring Security 實(shí)現(xiàn)自動(dòng)登錄功能
  9. Spring Boot 自動(dòng)登錄,安全風(fēng)險(xiǎn)要怎么控制?
  10. 在微服務(wù)項(xiàng)目中,Spring Security 比 Shiro 強(qiáng)在哪?
  11. SpringSecurity 自定義認(rèn)證邏輯的兩種方式(高級(jí)玩法)
  12. Spring Security 中如何快速查看登錄用戶 IP 地址等信息?

但是,在做 Spring Security 的 session 并發(fā)處理時(shí),直接將內(nèi)存中的用戶切換為數(shù)據(jù)庫(kù)中的用戶會(huì)有問(wèn)題,今天我們就來(lái)說(shuō)說(shuō)這個(gè)問(wèn)題,順便把這個(gè)功能應(yīng)用到微人事中(https://github.com/lenve/vhr )。

本文的案例將基于Spring Security+Spring Data Jpa 強(qiáng)強(qiáng)聯(lián)手,安全管理只有更簡(jiǎn)單!一文來(lái)構(gòu)建,所以重復(fù)的代碼我就不寫了,小伙伴們要是不熟悉可以參考該篇文章。

1.環(huán)境準(zhǔn)備

首先,我們打開Spring Security+Spring Data Jpa 強(qiáng)強(qiáng)聯(lián)手,安全管理只有更簡(jiǎn)單!一文中的案例,這個(gè)案例結(jié)合 Spring Data Jpa 將用戶數(shù)據(jù)存儲(chǔ)到數(shù)據(jù)庫(kù)中去了。

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

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

并在 SecurityConfig 中對(duì)登錄頁(yè)面稍作配置:

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

這里都是常規(guī)配置,我就不再多說(shuō)。注意最后面我們將 session 數(shù)量設(shè)置為 1。

好了,配置完成后,我們啟動(dòng)項(xiàng)目,并行性多端登錄測(cè)試。

打開多個(gè)瀏覽器,分別進(jìn)行多端登錄測(cè)試,我們驚訝的發(fā)現(xiàn),每個(gè)瀏覽器都能登錄成功,每次登錄成功也不會(huì)踢掉已經(jīng)登錄的用戶!

這是怎么回事?

2.問(wèn)題分析

要搞清楚這個(gè)問(wèn)題,我們就要先搞明白 Spring Security 是怎么保存用戶對(duì)象和 session 的。

Spring Security 中通過(guò) SessionRegistryImpl 類來(lái)實(shí)現(xiàn)對(duì)會(huì)話信息的統(tǒng)一管理,我們來(lái)看下這個(gè)類的源碼(部分):

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;
 });
 }

}

這個(gè)類的源碼還是比較長(zhǎng),我這里提取出來(lái)一些比較關(guān)鍵的部分:

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

看到這里,大家發(fā)現(xiàn)一個(gè)問(wèn)題,ConcurrentMap 集合的 key 是 principal 對(duì)象,用對(duì)象做 key,一定要重寫 equals 方法和 hashCode 方法,否則第一次存完數(shù)據(jù),下次就找不到了,這是 JavaSE 方面的知識(shí),我就不用多說(shuō)了。

如果我們使用了基于內(nèi)存的用戶,我們來(lái)看下 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();
 }
}

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

所以我們使用基于內(nèi)存的用戶時(shí)沒(méi)有問(wèn)題,而我們使用自定義的用戶就有問(wèn)題了。

找到了問(wèn)題所在,那么解決問(wèn)題就很容易了,重寫 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);
 }
 ...
 ...
}

配置完成后,重啟項(xiàng)目,再去進(jìn)行多端登錄測(cè)試,發(fā)現(xiàn)就可以成功踢掉已經(jīng)登錄的用戶了。

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

3.微人事應(yīng)用

3.1 存在的問(wèn)題

由于微人事目前是采用了 JSON 格式登錄,所以如果項(xiàng)目控制 session 并發(fā)數(shù),就會(huì)有一些額外的問(wèn)題要處理。

最大的問(wèn)題在于我們用自定義的過(guò)濾器代替了 UsernamePasswordAuthenticationFilter,進(jìn)而導(dǎo)致前面所講的關(guān)于 session 的配置,統(tǒng)統(tǒng)失效。所有相關(guān)的配置我們都要在新的過(guò)濾器 LoginFilter 中進(jìn)行配置 ,包括 SessionAuthenticationStrategy 也需要我們自己手動(dòng)配置了。

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

3.2 具體應(yīng)用

我們來(lái)看下具體怎么實(shí)現(xiàn),我這里主要列出來(lái)一些關(guān)鍵代碼,完整代碼大家可以從 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);
 }
 ...
 ...
}

接下來(lái)在 SecurityConfig 中進(jìn)行配置。

這里我們要自己提供 SessionAuthenticationStrategy,而前面處理 session 并發(fā)的是 ConcurrentSessionControlAuthenticationStrategy,也就是說(shuō),我們需要自己提供一個(gè) ConcurrentSessionControlAuthenticationStrategy 的實(shí)例,然后配置給 LoginFilter,但是在創(chuàng)建 ConcurrentSessionControlAuthenticationStrategy 實(shí)例的過(guò)程中,還需要有一個(gè) SessionRegistryImpl 對(duì)象。

前面我們說(shuō)過(guò),SessionRegistryImpl 對(duì)象是用來(lái)維護(hù)會(huì)話信息的,現(xiàn)在這個(gè)東西也要我們自己來(lái)提供,SessionRegistryImpl 實(shí)例很好創(chuàng)建,如下:

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

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

@Bean
LoginFilter 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;
}

我們?cè)谶@里自己手動(dòng)構(gòu)建 ConcurrentSessionControlAuthenticationStrategy 實(shí)例,構(gòu)建時(shí)傳遞 SessionRegistryImpl 參數(shù),然后設(shè)置 session 的并發(fā)數(shù)為 1,最后再將 sessionStrategy 配置給 LoginFilter。

其實(shí)上篇文章中,我們的配置方案,最終也是像上面這樣,只不過(guò)現(xiàn)在我們自己把這個(gè)寫出來(lái)了而已。

這就配置完了嗎?沒(méi)有!session 處理還有一個(gè)關(guān)鍵的過(guò)濾器叫做 ConcurrentSessionFilter,本來(lái)這個(gè)過(guò)濾器是不需要我們管的,但是這個(gè)過(guò)濾器中也用到了 SessionRegistryImpl,而 SessionRegistryImpl 現(xiàn)在是由我們自己來(lái)定義的,所以,該過(guò)濾器我們也要重新配置一下,如下:

@Override
protected 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("您已在另一臺(tái)設(shè)備登錄,本次登錄已下線!")));
 out.flush();
 out.close();
 }), ConcurrentSessionFilter.class);
 http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}

在這里,我們重新創(chuàng)建一個(gè) ConcurrentSessionFilter 的實(shí)例,代替系統(tǒng)默認(rèn)的即可。在創(chuàng)建新的 ConcurrentSessionFilter 實(shí)例時(shí),需要兩個(gè)參數(shù):

  • sessionRegistry 就是我們前面提供的 SessionRegistryImpl 實(shí)例。
  • 第二個(gè)參數(shù),是一個(gè)處理 session 過(guò)期后的回調(diào)函數(shù),也就是說(shuō),當(dāng)用戶被另外一個(gè)登錄踢下線之后,你要給什么樣的下線提示,就在這里來(lái)完成。

最后,我們還需要在處理完登錄數(shù)據(jù)之后,手動(dòng)向 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);
 } 
 ...
 ...
 }
}

在這里,我們手動(dòng)調(diào)用 sessionRegistry.registerNewSession 方法,向 SessionRegistryImpl 中添加一條 session 記錄。

OK,如此之后,我們的項(xiàng)目就配置完成了。

接下來(lái),重啟 vhr 項(xiàng)目,進(jìn)行多端登錄測(cè)試,如果自己被人踢下線了,就會(huì)看到如下提示:

完整的代碼,我已經(jīng)更新到 vhr 上了,大家可以下載學(xué)習(xí)。

4.小結(jié)

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

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

如果覺(jué)得有收獲,記得點(diǎn)個(gè)在看鼓勵(lì)下松哥哦~

相關(guān)文章

  • Java設(shè)計(jì)模式之迪米特原則精解

    Java設(shè)計(jì)模式之迪米特原則精解

    設(shè)計(jì)模式(Design pattern)代表了最佳的實(shí)踐,通常被有經(jīng)驗(yàn)的面向?qū)ο蟮能浖_發(fā)人員所采用。設(shè)計(jì)模式是軟件開發(fā)人員在軟件開發(fā)過(guò)程中面臨的一般問(wèn)題的解決方案。本篇介紹設(shè)計(jì)模式七大原則之一的迪米特原則
    2022-02-02
  • SpringBoot的Admin服務(wù)監(jiān)控詳解

    SpringBoot的Admin服務(wù)監(jiān)控詳解

    這篇文章主要介紹了SpringBoot的Admin服務(wù)監(jiān)控詳解,Spring Boot Admin(SBA)是一個(gè)開源的社區(qū)項(xiàng)目,用于管理和監(jiān)控 Spring Boot 應(yīng)用程序,需要的朋友可以參考下
    2024-01-01
  • MyBatis discriminator標(biāo)簽原理實(shí)例解析

    MyBatis discriminator標(biāo)簽原理實(shí)例解析

    這篇文章主要為大家介紹了MyBatis discriminator標(biāo)簽實(shí)現(xiàn)原理實(shí)例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-02-02
  • Spring MVC的項(xiàng)目準(zhǔn)備和連接建立方法

    Spring MVC的項(xiàng)目準(zhǔn)備和連接建立方法

    SpringWebMVC是基于Servlet API的Web框架,屬于Spring框架的一部分,主要用于簡(jiǎn)化Web應(yīng)用程序的開發(fā),SpringMVC通過(guò)控制器接收請(qǐng)求,使用模型處理數(shù)據(jù),并通過(guò)視圖展示結(jié)果,感興趣的朋友跟隨小編一起看看吧
    2024-10-10
  • spring security如何擴(kuò)展自定義登錄

    spring security如何擴(kuò)展自定義登錄

    本文詳細(xì)介紹了Spring Security的認(rèn)證原理和具體實(shí)現(xiàn),認(rèn)證原理基于過(guò)濾器鏈,通過(guò)驗(yàn)證用戶憑證和構(gòu)建認(rèn)證對(duì)象來(lái)保護(hù)應(yīng)用程序資源,實(shí)現(xiàn)自定義認(rèn)證功能的步驟包括創(chuàng)建自定義認(rèn)證提供程序、實(shí)現(xiàn)UserDetailsService接口以及在配置類中進(jìn)行相應(yīng)的配置
    2024-11-11
  • java中的equals()和toString()方法實(shí)例詳解

    java中的equals()和toString()方法實(shí)例詳解

    這篇文章主要介紹了java中的equals()和toString()方法實(shí)例詳解的相關(guān)資料,這里舉例說(shuō)明,并附實(shí)例代碼,和實(shí)現(xiàn)效果圖,需要的朋友可以參考下
    2016-11-11
  • java實(shí)現(xiàn)的日期時(shí)間轉(zhuǎn)換工具類完整示例

    java實(shí)現(xiàn)的日期時(shí)間轉(zhuǎn)換工具類完整示例

    這篇文章主要介紹了java實(shí)現(xiàn)的日期時(shí)間轉(zhuǎn)換工具類,結(jié)合完整實(shí)例形式分析了java針對(duì)日期時(shí)間常見的轉(zhuǎn)換、計(jì)算、格式化等相關(guān)操作與封裝技巧,需要的朋友可以參考下
    2019-10-10
  • JDBC用IDEA連接SQLServer數(shù)據(jù)庫(kù)的超實(shí)用教程

    JDBC用IDEA連接SQLServer數(shù)據(jù)庫(kù)的超實(shí)用教程

    JDBC是Java連接數(shù)據(jù)庫(kù)的一種接口,它由各個(gè)數(shù)據(jù)庫(kù)廠商為開發(fā)者提供的接口,要使用它需要到相應(yīng)廠商下載對(duì)應(yīng)的jar包,下面這篇文章主要給大家介紹了關(guān)于JDBC用IDEA連接SQLServer數(shù)據(jù)庫(kù)的超實(shí)用教程,需要的朋友可以參考下
    2023-05-05
  • java序列化和java反序列化示例

    java序列化和java反序列化示例

    在web項(xiàng)目開發(fā)的時(shí)候,經(jīng)常用到序列化和反序列化用來(lái)傳遞大流量的數(shù)據(jù),類只有實(shí)現(xiàn)Serializable借口才能被序列化,下來(lái)是java序列化和反序列化演示
    2014-01-01
  • Java中的ConcurrentLinkedQueue松散隊(duì)列解析

    Java中的ConcurrentLinkedQueue松散隊(duì)列解析

    這篇文章主要介紹了Java中的ConcurrentLinkedQueue松散隊(duì)列解析,鏈表是松散的,鏈表節(jié)點(diǎn)并不都是有效的,允許存在無(wú)效節(jié)點(diǎn)val=null,但是只有最后一個(gè)節(jié)點(diǎn)才能next=null,需要的朋友可以參考下
    2023-12-12

最新評(píng)論

玉溪市| 炉霍县| 灵川县| 陆良县| 白山市| 长兴县| 西安市| 红原县| 宁河县| 新宾| 新闻| 华坪县| 永兴县| 镇平县| 珠海市| 巴东县| 灌云县| 汉中市| 阜城县| 凤翔县| 交口县| 台州市| 沙田区| 洛宁县| 临汾市| 宾阳县| 志丹县| 大洼县| 纳雍县| 资溪县| 阿合奇县| 双峰县| 兴化市| 荥经县| 怀宁县| 璧山县| 无锡市| 肥城市| 高要市| 宾川县| 曲麻莱县|