热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

SpringBoot+Vue前后端分离项目如何踢掉已登录用户

这篇文章主要介绍了SpringBoot+Vue前后端分离项目如何踢掉已登录用户,需要的朋友可以参考下

上篇文章中,我们讲了在 Spring Security 中如何踢掉前一个登录用户,或者禁止用户二次登录,通过一个简单的案例,实现了我们想要的效果。

但是有一个不太完美的地方,就是我们的用户是配置在内存中的用户,我们没有将用户放到数据库中去。正常情况下,松哥在 Spring Security 系列中讲的其他配置,大家只需要参考Spring Security+Spring Data Jpa 强强联手,安全管理只有更简单!一文,将数据切换为数据库中的数据即可。

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

  1. 挖一个大坑,Spring Security 开搞!
  2. 松哥手把手带你入门 Spring Security,别再问密码怎么解密了
  3. 手把手教你定制 Spring Security 中的表单登录
  4. Spring Security 做前后端分离,咱就别做页面跳转了!统统 JSON 交互
  5. Spring Security 中的授权操作原来这么简单
  6. Spring Security 如何将用户数据存入数据库?
  7. Spring Security+Spring Data Jpa 强强联手,安全管理只有更简单!
  8. Spring Boot + Spring Security 实现自动登录功能
  9. Spring Boot 自动登录,安全风险要怎么控制?
  10. 在微服务项目中,Spring Security 比 Shiro 强在哪?
  11. SpringSecurity 自定义认证逻辑的两种方式(高级玩法)
  12. 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 中对登录页面稍作配置:

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

这里都是常规配置,我就不再多说。注意最后面我们将 session 数量设置为 1。

好了,配置完成后,我们启动项目,并行性多端登录测试。

打开多个浏览器,分别进行多端登录测试,我们惊讶的发现,每个浏览器都能登录成功,每次登录成功也不会踢掉已经登录的用户!

这是怎么回事?

2.问题分析

要搞清楚这个问题,我们就要先搞明白 Spring Security 是怎么保存用户对象和 session 的。

Spring Security 中通过 SessionRegistryImpl 类来实现对会话信息的统一管理,我们来看下这个类的源码(部分):

public class SessionRegistryImpl implements SessionRegistry,
 ApplicationListener {
 /**  */
 private final ConcurrentMap> principals;
 /**  */
 private final Map 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 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 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 实例很好创建,如下:

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

我们在这里自己手动构建 ConcurrentSessionControlAuthenticationStrategy 实例,构建时传递 SessionRegistryImpl 参数,然后设置 session 的并发数为 1,最后再将 sessionStrategy 配置给 LoginFilter。

其实上篇文章中,我们的配置方案,最终也是像上面这样,只不过现在我们自己把这个写出来了而已。

这就配置完了吗?没有!session 处理还有一个关键的过滤器叫做 ConcurrentSessionFilter,本来这个过滤器是不需要我们管的,但是这个过滤器中也用到了 SessionRegistryImpl,而 SessionRegistryImpl 现在是由我们自己来定义的,所以,该过滤器我们也要重新配置一下,如下:

@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("您已在另一台设备登录,本次登录已下线!")));
 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 项目,进行多端登录测试,如果自己被人踢下线了,就会看到如下提示:

完整的代码,我已经更新到 vhr 上了,大家可以下载学习。

4.小结

好了,本文主要和小伙伴们介绍了一个在 Spring Security 中处理 session 并发问题时,可能遇到的一个坑,以及在前后端分离情况下,如何处理 session 并发问题。不知道小伙伴们有没有 GET 到呢?

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

如果觉得有收获,记得点个在看鼓励下松哥哦~


推荐阅读
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • Java验证码——kaptcha的使用配置及样式
    本文介绍了如何使用kaptcha库来实现Java验证码的配置和样式设置,包括pom.xml的依赖配置和web.xml中servlet的配置。 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 禁止程序接收鼠标事件的工具_VNC Viewer for Mac(远程桌面工具)免费版
    VNCViewerforMac是一款运行在Mac平台上的远程桌面工具,vncviewermac版可以帮助您使用Mac的键盘和鼠标来控制远程计算机,操作简 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • 推荐系统遇上深度学习(十七)详解推荐系统中的常用评测指标
    原创:石晓文小小挖掘机2018-06-18笔者是一个痴迷于挖掘数据中的价值的学习人,希望在平日的工作学习中,挖掘数据的价值, ... [详细]
  • 如何在服务器主机上实现文件共享的方法和工具
    本文介绍了在服务器主机上实现文件共享的方法和工具,包括Linux主机和Windows主机的文件传输方式,Web运维和FTP/SFTP客户端运维两种方式,以及使用WinSCP工具将文件上传至Linux云服务器的操作方法。此外,还介绍了在迁移过程中需要安装迁移Agent并输入目的端服务器所在华为云的AK/SK,以及主机迁移服务会收集的源端服务器信息。 ... [详细]
  • 使用正则表达式爬取36Kr网站首页新闻的操作步骤和代码示例
    本文介绍了使用正则表达式来爬取36Kr网站首页所有新闻的操作步骤和代码示例。通过访问网站、查找关键词、编写代码等步骤,可以获取到网站首页的新闻数据。代码示例使用Python编写,并使用正则表达式来提取所需的数据。详细的操作步骤和代码示例可以参考本文内容。 ... [详细]
  • 本文介绍了前端人员必须知道的三个问题,即前端都做哪些事、前端都需要哪些技术,以及前端的发展阶段。初级阶段包括HTML、CSS、JavaScript和jQuery的基础知识。进阶阶段涵盖了面向对象编程、响应式设计、Ajax、HTML5等新兴技术。高级阶段包括架构基础、模块化开发、预编译和前沿规范等内容。此外,还介绍了一些后端服务,如Node.js。 ... [详细]
  • 在Android中解析Gson解析json数据是很方便快捷的,可以直接将json数据解析成java对象或者集合。使用Gson解析json成对象时,默认将json里对应字段的值解析到java对象里对应字段的属性里面。然而,当我们自己定义的java对象里的属性名与json里的字段名不一样时,我们可以使用@SerializedName注解来将对象里的属性跟json里字段对应值匹配起来。本文介绍了使用@SerializedName注解解析json数据的方法,并给出了具体的使用示例。 ... [详细]
  • Java学习笔记之使用反射+泛型构建通用DAO
    本文介绍了使用反射和泛型构建通用DAO的方法,通过减少代码冗余度来提高开发效率。通过示例说明了如何使用反射和泛型来实现对不同表的相同操作,从而避免重复编写相似的代码。该方法可以在Java学习中起到较大的帮助作用。 ... [详细]
  • 原理:dismiss再弹出,把dialog设为全局对象。if(dialog!null&&dialog.isShowing()&&!(Activity.)isFinishing()) ... [详细]
author-avatar
单纯的猪猪zhu
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有