【Spring Security】如何实现多设备同一时间只允许一个账号登录(即前登录用户被后登录用户挤下线)?只需简单两步!

1.需求分析

在同一个系统中,我们可能只允许一个用户在一个终端上登录,一般来说这可能是出于安全方面的考虑,但是也有一些情况是出于业务上的考虑,需求就是业务原因要求一个用户只能在一个设备上登录。

要实现一个用户不可以同时在两台设备上登录,我们有两种思路:

  • 后来的登录自动踢掉前面的登录。
  • 如果用户已经登录,则不允许后来者登录。

这种思路都能实现这个功能,具体使用哪一个,还要看我们具体的需求。

在 Spring Security 中,这两种都很好实现,一个配置就可以搞定。

2.具体实现

2.1 踢掉已经登录用户

想要用新的登录踢掉旧的登录,我们只需要将最大会话数设置为 1 即可,配置如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login.html")
            .permitAll()
            .and()
            .csrf().disable()
            .sessionManagement()
            .maximumSessions(1);
}

maximumSessions 表示配置最大会话数为 1,这样后面的登录就会自动踢掉前面的登录。这里其他的配置都是我们前面文章讲过的,我就不再重复介绍,配置完成后,用 Chrome 浏览器进行测试(可以利用 Chrome 中隐私模式登录模拟多用户功能)。

  1. Chrome 上登录成功后,访问 /hello 接口。
  2. 利用 Chrome隐私模式模式上登录成功后,访问 /hello 接口。
  3. 在 Chrome 上再次访问 /hello 接口,此时会看到如下提示:
This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).

可以看到,这里说这个 session 已经过期,原因则是由于使用同一个用户进行并发登录。
注意:如果你自定义用户user直接实现UserDetails接口的实体类,需要将复写toString、hashCode、equals 函数,不然看不到效果,原因也很简单,不复写两个用户实例对象比较会有问题!代码如下:

    @Override
    public String toString() {
        return this.username;
    }
    @Override
    public int hashCode() {
        return username.hashCode();
    }
    @Override
    public boolean equals(Object obj) {
        return this.toString().equals(obj.toString());
    }

2.2 禁止新的登录

如果相同的用户已经登录了,你不想踢掉他,而是想禁止新的登录操作,那也好办,配置方式如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login.html")
            .permitAll()
            .and()
            .csrf().disable()
            .sessionManagement()
            .maximumSessions(1)
            .maxSessionsPreventsLogin(true);
}

添加 maxSessionsPreventsLogin 配置即可。此时一个浏览器登录成功后,另外一个浏览器就登录不了了。是不是很简单?

不过还没完,我们还需要再提供一个 Bean:

@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}

为什么要加这个 Bean 呢?因为在 Spring Security 中,它是通过监听 session 的销毁事件,来及时的清理 session 的记录。用户从不同的浏览器登录后,都会有对应的 session,当用户注销登录之后,session 就会失效,但是默认的失效是通过调用 StandardSession#invalidate 方法来实现的,这一个失效事件无法被 Spring 容器感知到,进而导致当用户注销登录之后,Spring Security 没有及时清理会话信息表,以为用户还在线,进而导致用户无法重新登录进来(小伙伴们可以自行尝试不添加上面的 Bean,然后让用户注销登录之后再重新登录)。

为了解决这一问题,我们提供一个 HttpSessionEventPublisher ,这个类实现了 HttpSessionListener 接口,在该 Bean 中,可以将 session 创建以及销毁的事件及时感知到,并且调用 Spring 中的事件机制将相关的创建和销毁事件发布出去,进而被 Spring Security 感知到,该类部分源码如下:

public void sessionCreated(HttpSessionEvent event) {
 HttpSessionCreatedEvent e = new HttpSessionCreatedEvent(event.getSession());
 getContext(event.getSession().getServletContext()).publishEvent(e);
}
public void sessionDestroyed(HttpSessionEvent event) {
 HttpSessionDestroyedEvent e = new HttpSessionDestroyedEvent(event.getSession());
 getContext(event.getSession().getServletContext()).publishEvent(e);
}

OK,虽然多了一个配置,但是依然很简单!

3.实现原理

上面这个功能,在 Spring Security 中是怎么实现的呢?我们来稍微分析一下源码。

首先我们知道,在用户登录的过程中,会经过 UsernamePasswordAuthenticationFilter(参考:手把手带你捋一遍 Spring Security 登录流程),而 UsernamePasswordAuthenticationFilter 中过滤方法的调用是在 AbstractAuthenticationProcessingFilter 中触发的,我们来看下 AbstractAuthenticationProcessingFilter#doFilter 方法的调用:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
  throws IOException, ServletException {
 HttpServletRequest request = (HttpServletRequest) req;
 HttpServletResponse response = (HttpServletResponse) res;
 if (!requiresAuthentication(request, response)) {
  chain.doFilter(request, response);
  return;
 }
 Authentication authResult;
 try {
  authResult = attemptAuthentication(request, response);
  if (authResult == null) {
   return;
  }
  sessionStrategy.onAuthentication(authResult, request, response);
 }
 catch (InternalAuthenticationServiceException failed) {
  unsuccessfulAuthentication(request, response, failed);
  return;
 }
 catch (AuthenticationException failed) {
  unsuccessfulAuthentication(request, response, failed);
  return;
 }
 // Authentication success
 if (continueChainBeforeSuccessfulAuthentication) {
  chain.doFilter(request, response);
 }
 successfulAuthentication(request, response, chain, authResult);

在这段代码中,我们可以看到,调用 attemptAuthentication 方法走完认证流程之后,回来之后,接下来就是调用 sessionStrategy.onAuthentication 方法,这个方法就是用来处理 session 的并发问题的。具体在:

public class ConcurrentSessionControlAuthenticationStrategy implements
  MessageSourceAware, SessionAuthenticationStrategy {
 public void onAuthentication(Authentication authentication,
   HttpServletRequest request, HttpServletResponse response) {

  final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
    authentication.getPrincipal(), false);

  int sessionCount = sessions.size();
  int allowedSessions = getMaximumSessionsForThisUser(authentication);

  if (sessionCount < allowedSessions) {
   // They haven't got too many login sessions running at present
   return;
  }

  if (allowedSessions == -1) {
   // We permit unlimited logins
   return;
  }

  if (sessionCount == allowedSessions) {
   HttpSession session = request.getSession(false);

   if (session != null) {
    // Only permit it though if this request is associated with one of the
    // already registered sessions
    for (SessionInformation si : sessions) {
     if (si.getSessionId().equals(session.getId())) {
      return;
     }
    }
   }
   // If the session is null, a new one will be created by the parent class,
   // exceeding the allowed number
  }

  allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);
 }
 protected void allowableSessionsExceeded(List<SessionInformation> sessions,
   int allowableSessions, SessionRegistry registry)
   throws SessionAuthenticationException {
  if (exceptionIfMaximumExceeded || (sessions == null)) {
   throw new SessionAuthenticationException(messages.getMessage(
     "ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
     new Object[] {allowableSessions},
     "Maximum sessions of {0} for this principal exceeded"));
  }

  // Determine least recently used sessions, and mark them for invalidation
  sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
  int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
  List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
  for (SessionInformation session: sessionsToBeExpired) {
   session.expireNow();
  }
 }
}

这段核心代码我来给大家稍微解释下:

  1. 首先调用 sessionRegistry.getAllSessions 方法获取当前用户的所有 session,该方法在调用时,传递两个参数,一个是当前用户的 authentication,另一个参数 false 表示不包含已经过期的 session(在用户登录成功后,会将用户的 sessionid 存起来,其中 key 是用户的主体(principal),value 则是该主题对应的 sessionid 组成的一个集合)。
  2. 接下来计算出当前用户已经有几个有效 session 了,同时获取允许的 session 并发数。
  3. 如果当前 session 数(sessionCount)小于 session 并发数(allowedSessions),则不做任何处理;如果 allowedSessions 的值为 -1,表示对 session 数量不做任何限制。
  4. 如果当前 session 数(sessionCount)等于 session 并发数(allowedSessions),那就先看看当前 session 是否不为 null,并且已经存在于 sessions 中了,如果已经存在了,那都是自家人,不做任何处理;如果当前 session 为 null,那么意味着将有一个新的 session 被创建出来,届时当前 session 数(sessionCount)就会超过 session 并发数(allowedSessions)。
  5. 如果前面的代码中都没能 return 掉,那么将进入策略判断方法 allowableSessionsExceeded 中。
  6. allowableSessionsExceeded 方法中,首先会有 exceptionIfMaximumExceeded 属性,这就是我们在 SecurityConfig 中配置的 maxSessionsPreventsLogin 的值,默认为 false,如果为 true,就直接抛出异常,那么这次登录就失败了(对应 2.2 小节的效果),如果为 false,则对 sessions 按照请求时间进行排序,然后再使多余的 session 过期即可(对应 2.1 小节的效果)。

4.小结

如此,两行简单的配置就实现了 Spring Security 中 session 的并发管理。是不是很简单?该项目源码及往期教程我已经放在码云上了,有需要的朋友可以下载看看https://gitee.com/wavefar/spring-security-samples,不过这里还有一个小小的坑,我们将在下篇文章中继续和大家分析。

<think>好的,我现在需要为用户提供一个关于码云(Gitee)的使用指南。首先,用户已经询问过开源软件的相关知识,现在转向具体的工具使用,说明他们可能已经对开源有基本了解,现在想实际操作。我需要确保指南结构清晰,步骤明确,适合层次的用户,尤其是可能的新手。 首先,应该先介绍Gitee是什么,和GitHub的关系,以及它的主要特点,比如国内访问速度快、支持中文等。这有助于用户理解为什么选择Gitee。 接下来,需要分步骤说明如何使用。注册和登录是第一步,应该简单明了,提到支持的注册方式,比如手机、邮箱或第三方账号。然后创建仓库是基础操作,需要详细说明每个字段的作用,比如仓库名称、描述、公开性等。 然后,配置Git环境是必要的,特别是对于新手,可能需要指导如何安装Git和设置用户名邮箱。这里需要给出具体的命令示例,并解释每个命令的作用,确保用户会出错。 仓库操作部分包括克隆、提交代码、推送和拉取。每个步骤都需要具体命令,比如git clone、git add、git commit、git push等,并解释每个命令的意义。需要提醒用户注意分支管理和冲突解决,但可能需要深入,保持基础即可。 协开发功能如Issue、Pull Request和Wiki是团队合作的关键,需要简单说明如何使用这些功能,但可能需要后续更详细的指南。样,代码托管外的功能如Pages和CI/CD可以提及,但用户可能暂时用到,简要介绍即可。 最后,注意事项部分需要强调遵守开源协议、定期备份和社区互动,帮助用户避免常见问题。时,提供学习资源链接,方便用户进一步学习。 需要检查是否符合之的系统级指令,比如行内公式用$...$,独立公式用$$...$$,但在这个指南中可能用上数学公式,所以需要确认是否有遗漏。另外,确保使用中文,结构清晰,分点说明,使用代码块包裹命令,增强可读性。 可能还需要考虑用户可能遇到的问题,比如Git配置错误、推送权限问题等,但作为基础指南,可能需要深入这些问题,保持简洁。需要确保所有步骤准确,命令正确,避免误导用户。最后,总结部分要强调Gitee的优势,鼓励用户实践。</think>以下是**码云(Gitee)**的使用指南,涵盖从注册到协开发的核心操作: --- ### 一、Gitee简介 - **定位**:中国本土的代码托管平台,类似GitHub,支持Git版本控制。 - **优势**: - 国内访问速度快,支持中文界面 - 提供私有仓库(免费用户可创建)、代码审查、CI/CD等工具 - 适合个人开发者、企业及开源项目协作 --- ### 二、基础操作步骤 #### 1. 注册与登录 - 访问[Gitee官网](https://gitee.com),点击“注册” - 支持手机号、邮箱或第三方账号(微信、GitHub)注册 #### 2. 创建仓库 - 登录后点击右上角 ➕ → **新建仓库** - 填写信息: - **仓库名称**:建议英文(如`my-project`) - **描述**:简要说明项目用途 - **公开性**:选择“公开”或“私有” - **初始化**:可选添加README文件或.gitignore模板 #### 3. 配置Git环境 - **安装Git**:从[官网](https://git-scm.com)下载并安装 - **设置用户名和邮箱**(与Gitee账号一致): ```bash git config --global user.name "你的用户名" git config --global user.email "你的Gitee注册邮箱" ``` --- ### 三、仓库操作 #### 1. 克隆仓库到本地 - 进入仓库页面 → 点击“克隆/下载” → 复制HTTPS或SSH链接 - 执行命令: ```bash git clone https://gitee.com/你的用户名/仓库名.git ``` #### 2. 提交与推送代码 - **添加文件到暂存区**: ```bash git add . # 添加所有文件 git add filename # 添加指定文件 ``` - **提交更改**: ```bash git commit -m "提交描述" ``` - **推送到远程仓库**: ```bash git push origin master # 默认分支为master或main ``` #### 3. 拉取更新 - 步远程仓库的最新代码: ```bash git pull origin master ``` --- ### 四、协开发功能 #### 1. 成员协作 - **添加协作者**: 仓库页面 → “管理” → “成员管理” → 输入用户账号邀请 - **团队组织**: 创建“组织” → 统一管理多个仓库和成员权限 #### 2. Issue跟踪 - 用于任务分配、Bug反馈: 仓库页面 → “Issues” → “新建Issue” → 填写标题和描述 #### 3. Pull Request(PR) - 合并代码更改的流程: 1. Fork他人仓库 → 本地修改后推送到自己的仓库 2. 发起PR → 等待原作者审核合并 --- ### 五、进阶功能 #### 1. Gitee Pages - 静态网页托管: 仓库 → “服务” → “Gitee Pages” → 选择分支部署 #### 2. CI/CD(持续集成) - 通过`.gitee.yml`配置文件自动化测试和部署: ```yaml # 示例:Node.js项目构建 stages: - install - build install: - npm install build: - npm run build ``` --- ### 六、注意事项 1. **分支管理**:避免直接在`master`分支开发,建议新建功能分支(如`feat-login`) 2.
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

52it.club

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值