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

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

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

但是,在做 Spring Security 的 session 并发处理时,直接将内存中的用户切换为数据库中的用户会有问题,今天我们就来说说这个问题,顺便把这个功能应用到微人事中(https://github.com/lenve/vhr )。

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 到呢?

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

java怎么实现踢掉在线用户_Spring Boot + Vue 前后端分离项目如何踢掉已登录用户...相关推荐

  1. phython在file同时写入两个_喜大普奔,两个开源的 Spring Boot + Vue 前后端分离项目可以在线体验了

    折腾了一周的域名备案昨天终于搞定了. 松哥第一时间想到赶紧把微人事和 V 部落部署上去,我知道很多小伙伴已经等不及了. 1. 也曾经上过线 其实这两个项目当时刚做好的时候,我就把它们部署到服务器上了, ...

  2. .vue文件_Spring Boot + Vue 前后端分离,两种文件上传方式总结!

    在Vue.js 中,如果网络请求使用 axios ,并且使用了 ElementUI 库,那么一般来说,文件上传有两种不同的实现方案: 通过 Ajax 实现文件上传 通过 ElementUI 里边的 U ...

  3. vue前后端分离项目http请求携带cookie设置,java过滤器filter设置允许跨域

    最近使用VUE开发前后端分离,登录后,后端存入用户信息到session,前端http axios异步请求获取不到sesion,查看http请求头发现http headers未携带cookie, 然后查 ...

  4. 开源~~~~spring boot +vue 前后端分离 在线考试系统 加自动组卷!!!!

    在线考试系统+自动组卷!!! springboot +vue 前后端分离系统 想要源码的可以B站搜索 技术小余哥

  5. java 同域名下怎么访问同事的项目_喜大普奔,两个开源的前后端分离项目可以在线体验了...

    折腾了一周的域名备案昨天终于搞定了. 松哥第一时间想到赶紧把微人事和V 部落上去,我知道很多小伙伴已经等不及了. 1. 也曾经上过线 其实这两个项目当时刚做好的时候,我就把它们部署到服务器上了,以帮助 ...

  6. Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)十六(商品排序,Thymeleaf快速入门,商品详情页的展示)

    Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)十六(商品详情页的展示) 一.商品排序 1.完善页面信息 这是用来做排序的,默认按照综合排序 ...

  7. Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)二十二(下单和微信支付)

    Java网络商城项目 SpringBoot+SpringCloud+Vue 网络商城(SSM前后端分离项目)二十(下单) 0.学习目标 会调用订单系统接口 实现订单结算功能 实现微信支付功能 1.订单 ...

  8. 基于SpringBoot+Vue前后端分离的在线教育平台项目

    基于SpringBoot+Vue前后端分离的在线教育平台项目 赠给有缘人,希望能帮助到你!也请不要吝惜你的大拇指,你的Star.点赞将是对我最大的鼓励与支持! 开源传送门: 后台:Gitee | Gi ...

  9. 视频教程-SpringBoot实战教程:SpringBoot入门及前后端分离项目开发-Java

    SpringBoot实战教程:SpringBoot入门及前后端分离项目开发 十三,CSDN达人课课程作者,CSDN 博客作者,现就职于某网络科技公司任职高级 Java 开发工程师,13blog.sit ...

  10. 视频教程-Vue、Spring Boot开发小而完整的Web前后端分离项目实战-Java

    Vue.Spring Boot开发小而完整的Web前后端分离项目实战 3年多.net开发经验:5年的java后端开发经验,熟悉行.net,java流行技术,拥有多个.net,java web企业级应; ...

最新文章

  1. -bash: xxx: command not found
  2. Automatically populating $HTTP_RAW_POST_DATA is deprecated and will be removed in a future version.
  3. 食物链(信息学奥赛一本通-T1390)
  4. 在Ubuntu中为root用户启用界面登录
  5. MyBatis之使用XML配置SQL映射(二)CRUD映射配置
  6. 查看Jetson系列产品JetPack的版本信息
  7. .bat脚本基本命令合集
  8. 金融科技领域的安全威胁及金融科技安全分析
  9. 行测题中逻辑判断题的规律
  10. 文件上传2-搭建uploads靶场
  11. 服务器被攻击怎么处理
  12. 想做一个SSL代理的集群中,有很多问题需要解决,I am all grateful to you for your advice
  13. 双路cpu比单路强多少_别傻了!双核和双路服务器根本不一样
  14. 开发者笑疯了! LLaMa惊天泄露引爆ChatGPT平替狂潮,开源LLM领域变天
  15. python opencv 利用HSV,YUV(YCbCr)实现皮肤检测与抠图,与磨皮美颜
  16. 蓝桥杯2017 包子凑数
  17. 什么是api_什么是API
  18. 粗览Activiti Modeler操作和源代码
  19. 3B大战 GS很受伤
  20. 轮播图自动滚动 无缝连接 js

热门文章

  1. python爬虫可以做哪些好玩的地方_如何快速的找到好玩的旅游景点信息?Python爬虫帮你轻松解决...
  2. 三维旋转矩阵_线性代数的本质(4)--矩阵乘法与复合变换
  3. c语言中字符名词解释,C语言名词解释哪里有名词的解释 比如 什么型 什么型的...
  4. h5跳转小程序页面url_小程序和h5跳转
  5. JS内置对象方法——array
  6. Cookie enable 的检测
  7. openstarck安装指南(图文详解,超小白版本)
  8. NYOJ -11--奇偶数分离
  9. 什么是最佳的视频用户体验?阿里云视频服务四大体验优化实践
  10. [后端人员耍前端系列]AngularJs篇:使用AngularJs打造一个简易权限系统