通常系统都会限制同一个账号的登录人数,多人登录要么限制后者登录,要么踢出前者,Spring Security 提供了这样的功能,本文讲解一下在没有使用Security的时候如何手动实现这个功能

技术选型

  • SpringBoot

  • JWT

  • Filter

  • Redis + Redisson

JWT(token)存储在Redis中,类似 JSessionId-Session的关系,用户登录后每次请求在Header中携带jwt

如果你是使用session的话,也完全可以借鉴本文的思路,只是代码上需要加些改动

两种实现思路

1、比较时间戳

维护一个 username:jwtToken 这样的一个 key-value 在Reids中, Filter逻辑如下

package com.gitee.taven.filter;

import com.gitee.taven.utils.JWTUtil;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBucket;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 比较时间戳
 */
public class CompareKickOutFilter extends KickOutFilter {
    @Override
    public boolean isAccessAllowed(HttpServletRequest request, HttpServletResponse response) {
        String token = request.getHeader("Authorization");
        String username = JWTUtil.getUsername(token);
        String userKey = PREFIX + username;

RBucket<String> bucket = redissonClient.getBucket(userKey);
        String redisToken = bucket.get();

if (token.equals(redisToken)) {
            return true;

} else if (StringUtils.isBlank(redisToken)) {
            bucket.set(token);

} else {
            Long redisTokenUnixTime = JWTUtil.getClaim(redisToken, "createTime").asLong();
            Long tokenUnixTime = JWTUtil.getClaim(token, "createTime").asLong();

// token > redisToken 则覆盖
            if (tokenUnixTime.compareTo(redisTokenUnixTime) > 0) {
                bucket.set(token);

} else {
                // 注销当前token
                userService.logout(token);
                sendJsonResponse(response, 4001, "您的账号已在其他设备登录");
                return false;

}

}

return true;

}
}
2、队列踢出

package com.gitee.taven.filter;

import com.gitee.taven.pojo.CurrentUser;
import com.gitee.taven.pojo.UserBO;
import org.redisson.api.RBucket;
import org.redisson.api.RDeque;
import org.redisson.api.RLock;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

/**
 * 队列踢出
 */
public class QueueKickOutFilter extends KickOutFilter {
    /**
     * 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
     */
    private boolean kickoutAfter = false;
    /**
     * 同一个帐号最大会话数 默认1
     */
    private int maxSession = 1;

public void setKickoutAfter(boolean kickoutAfter) {
        this.kickoutAfter = kickoutAfter;
    }

public void setMaxSession(int maxSession) {
        this.maxSession = maxSession;
    }

@Override
    public boolean isAccessAllowed(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String token = request.getHeader("Authorization");
        UserBO currentSession = CurrentUser.get();
        Assert.notNull(currentSession, "currentSession cannot null");
        String username = currentSession.getUsername();
        String userKey = PREFIX + "deque_" + username;
        String lockKey = PREFIX_LOCK + username;

RLock lock = redissonClient.getLock(lockKey);

lock.lock(2, TimeUnit.SECONDS);

try {
            RDeque<String> deque = redissonClient.getDeque(userKey);

// 如果队列里没有此token,且用户没有被踢出;放入队列
            if (!deque.contains(token) && currentSession.isKickout() == false) {
                deque.push(token);
            }

// 如果队列里的sessionId数超出最大会话数,开始踢人
            while (deque.size() > maxSession) {
                String kickoutSessionId;
                if (kickoutAfter) { // 如果踢出后者
                    kickoutSessionId = deque.removeFirst();
                } else { // 否则踢出前者
                    kickoutSessionId = deque.removeLast();
                }

try {
                    RBucket<UserBO> bucket = redissonClient.getBucket(kickoutSessionId);
                    UserBO kickoutSession = bucket.get();

if (kickoutSession != null) {
                        // 设置会话的kickout属性表示踢出了
                        kickoutSession.setKickout(true);
                        bucket.set(kickoutSession);
                    }

} catch (Exception e) {
                }

}

// 如果被踢出了,直接退出,重定向到踢出后的地址
            if (currentSession.isKickout()) {
                // 会话被踢出了
                try {
                    // 注销
                    userService.logout(token);
                    sendJsonResponse(response, 4001, "您的账号已在其他设备登录");

} catch (Exception e) {
                }

return false;

}

} finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
                LOGGER.info(Thread.currentThread().getName() + " unlock");

} else {
                LOGGER.info(Thread.currentThread().getName() + " already automatically release lock");
            }
        }

return true;
    }

}

比较两种方法

第一种方法逻辑简单粗暴, 只维护一个key-value 不需要使用锁,非要说缺点的话没有第二种方法灵活。

第二种方法我很喜欢,代码很优雅灵活,但是逻辑相对麻烦一些,而且为了保证线程安全地操作队列,要使用分布式锁。目前我们项目中使用的是第一种方法

演示

下载地址:https://gitee.com/yintianwen7/taven-springboot-learning/tree/master/login-control

1、运行项目,访问localhost:8887 demo中没有存储用户信息,随意输入用户名密码,用户名相同则被踢出

2、访问 localhost:8887/index.html 弹出用户信息, 代表当前用户有效

3、另一个浏览器登录相同用户名,回到第一个浏览器刷新页面,提示被踢出

4、application.properties中选择开启哪种过滤器模式,默认是比较时间戳踢出,开启队列踢出 queue-filter.enabled=true

原文:https://mp.weixin.qq.com/s?__biz=MzIwNTk5NjEzNw==&mid=2247489250&idx=1&sn=80802f78eae6dcd08f9b95b199516d89&chksm=97293fe4a05eb6f24c185c79ba21b9c33a900a5a536e12532b2c58773858813cf97ce12576b1&mpshare=1&scene=1&srcid=&sharer_sharetime=1579527404904&sharer_shareid=c835549f63a1862d2bae795978197c7e&key=a0d400227dbee67d052d3a96e095080c710f01d5bb087b262cd08d0d4127cbf6874874ba6effbbafa425ddbcc6d0208afdd6b0403dba22b8e44533b34de7ea7a7fde5aa376df7aac7da5299a5c1962f7&ascene=1&uin=MjIwODE4ODMyNQ%3D%3D&devicetype=Windows+10&version=6208006f&lang=zh_CN&exportkey=AwN2%2FfmM0ulwOggQ0Ath148%3D&pass_ticket=uAE2U5Ne1onz5miWjVRQhkfYIZUq3%2FLCNQWwXuTgKXrX8KlqIGz3ywoJLVUOncX6

5、关于控制登录人数的代码:https://github.com/xuexionghui/yintianwen7-taven-springboot-learning-master/tree/master/taven-springboot-learning/login-control

6、这个并发登录控制人数的项目,不需要依赖其他项目,可以直接使用,本项目可以独立运行
项目有两种方式控制登录人数,默认是开启时间戳对比控制登陆人数。
如果需要开启队列控制登录人数,就在配置文件application.properties不要注释queue-filter.enabled=true。
时间戳登录模式就是默认一个账号只可以登录一个,而队列方式就可以控制登录的人数。
CompareKickOutFilter.java  时间戳控制登录人数
QueueKickOutFilter.java  队列控制登录人数

模仿爱奇艺账号登录限制人数,SpringBoot 并发登录人数控制,踢人功能相关推荐

  1. 爱奇艺网络协程编写高并发应用实践

    作者 | 爱奇艺技术产品团队 责编 | 屠敏 出品 | CSDN 博客 本⽂以爱奇艺开源的网络协程库(https://github.com/iqiyi/libfiber )为例,讲解网络协程的设计原理 ...

  2. 模仿爱奇艺播放暂停按钮动画效果——swift

    先上效果图 实现思路: 重载init,画出左边线条.右边线条.三角形和圆弧图层,用layer.strokeEnd = 0隐藏三角形和弧线,初始化展示暂停按钮.圆弧作为过渡右边线和三角形使用. 暂停按钮 ...

  3. [经验教程]一个爱奇艺VIP会员帐号怎么共享给多个朋友家人使用同一个爱奇艺会员账号?

    爱奇艺VIP会员开通后又不能一直自己使用,在空闲时间就比较浪费.开通一个爱奇艺VIP会员账号怎么共享给多人(例如:爱人.家人.朋友)使用同一个爱奇艺会员账号享受VIP会员特权.虽然能够给爱人.家人.朋 ...

  4. 利用爱奇艺开放平台实现视频托管回调播放(一)——获取授权

    背景: 题库软件的教学视频需要实现在线播放,由于技术水平和服务器配置的限制,如果把视频放在自己服务器上会出现各种问题.访问人数少的时候还可以勉强应付,临近考试时,服务器压力增大,视频访问延迟太大,有时 ...

  5. 安卓模拟器安装教程_安卓模拟器爱奇艺青春有你2打榜助力教程

    本篇教程来教各位如何使用雷电安卓模拟器来进行青春有你2打榜助力.使用安卓模拟器打榜投票的好处就是一台电脑可以充当起码5-6部手机使用,而且能够同时操作,免去频繁切换账号的烦恼. 一.下载并安装雷电模拟 ...

  6. [经验教程]iPhone苹果手机上怎么使用微信支付123元开通爱奇艺京东plus联名会员?

    iPhone苹果手机上怎么使用微信支付123元开通爱奇艺京东plus联名会员? 1.打开爱奇艺京东plus官方联合会员活动页面: iPhone苹果手机上怎么使用微信支付123元开通爱奇艺京东plus联 ...

  7. 怎么取消苹果订阅自动续费_首开79,到期自动续费扣178元! 如何取消爱奇艺自动续费?...

    很多朋友通过了爱奇艺新用户99元一年的活动开通了爱奇艺黄金会员,目前爱奇艺官网在大力推广自动续费服务,自动续费首次开通比较优惠,但是到期后下次扣分价格就不怎么划算了,比如新用户首次开通最低79元一年( ...

  8. 验证手机号是否注册过爱奇艺

    接口名称 1) 请求地址 http://127.0.0.1/app/aiqiyi_validation.php 2) 调用方式:HTTP post 3) 接口描述: 接口描述详情 4) 请求参数: P ...

  9. 怎样取消连续包月自动续费_手机爱奇艺会员怎么取消自动续费 VIP关闭解除自动续费方法...

    爱奇艺会员办理提供了自动续费的功能,意思是会员快到期的话,就会自动付费继续开通,这样的操作给了一部分用户带来了便利,无需在刻意留意到期时间进行续费,不过也有很多小伙伴觉得不好,因为可能到期就不用了,这 ...

最新文章

  1. Building Fire Stations 39届亚洲赛牡丹江站B题
  2. Ubuntu下搭建NFS,并在开发板挂载
  3. CoreCLR源码探索(一) Object是什么
  4. C# 用Attribute实现AOP事务
  5. vivo X30新细节曝光:搭载潜望式超远摄支持双模5G
  6. Linux Tomcat9 控制界面及管理配置
  7. 2019年技术盘点容器篇(三):阿里专家谈容器:既叫好又叫座? | 程序员硬核评测
  8. qt.qpa.plugin:Cound not load the QT platform plugin “windows“ in “ “even though it was found.
  9. 内存中的存储空间(栈空间、堆空间、数据段、代码段)
  10. 物理-反重力系统:反重力系统
  11. 街道字符识别赛题理解
  12. 润乾报表数据集中参数和宏的使用方法
  13. 微信小程序 图片旋转后上传
  14. 国内物联网产业仍处初级阶段 运营商NB-IoT大有可为
  15. DP12 龙与地下城游戏问题
  16. CAD制图初学入门技巧:如何批量生成CAD填充边界?
  17. Androidnbsp;学习论坛博客及网站推荐(…
  18. 02 ABY框架的搭建及踩到的坑
  19. VCam 虚拟摄像头 V3.1.0 下载 - 天空软件站 - 聊天工具 - 联络聊天
  20. Hbase的Regina分区

热门文章

  1. python-05-字符串
  2. 我说CMMI2.0 之过程资产开发(PAD)
  3. python3 isinstance用法_python isinstance()方法的使用
  4. 扩展名.2k19sys勒索病毒如何清除,后缀.2k19sys勒索病毒分析以及如何恢复数据
  5. 在C#中,如何用最装逼的代码和最快的速度拷贝数组?
  6. 盒马鲜生如何筑起生鲜壁垒
  7. 基于python的简单异或脚本
  8. 最受大厂欢迎的30款开源项目
  9. 【C++】Winsock套接字编程,struct sockaddr、sockaddr_in,主机网络字节序
  10. js jquery控制input为只读