SpringBoot基于Redis+WebSocket 实现账号单设备登录.

引言

在现代应用中,一个账号在多个设备上的同时登录可能带来安全隐患。为了解决这个问题,许多应用实现了单设备登录,确保同一个用户只能在一个设备上登录。当用户在新的设备上登录时,旧设备会被强制下线。
本文将介绍如何使用 Spring Boot 和 Redis 来实现单设备登录功能。

效果图

在线访问地址: https://www.coderman.club/#/dashboard
在这里插入图片描述

思路

userId:xxx (被覆盖)
userId:yyy

  1. 用户登录时,新的 token 会覆盖 Redis 中的旧 token,确保每次登录都是最新的设备。
  2. 接口访问时,通过拦截器对 token 进行验证,确保同一时间只有一个有效会话。
  3. 如果 token 不匹配或过期,则拦截请求,返回未授权的响应。

代码实现

在这里插入图片描述

/*** 权限拦截器* @author coderman*/
@Aspect
@Component
@Order(value = AopConstant.AUTH_ASPECT_ORDER)
@Lazy(value = false)
@Slf4j
public class AuthAspect {/*** 白名单接口*/public static List<String> whiteListUrl = new ArrayList<>();/*** 资源url与功能关系*/public static Map<String, Set<Integer>> systemAllResourceMap = new HashMap<>();/*** 无需拦截的url且有登录信息*/public static List<String> unFilterHasLoginInfoUrl = new ArrayList<>();/*** 资源api*/@Resourceprivate RescService rescApi;/*** 用户api*/@Resourceprivate UserService userApi;/*** 是否单设备登录校验*/private static final boolean isOneDeviceLogin = true;@PostConstructpublic void init() {this.refreshSystemAllRescMap();}/*** 刷新系统资源*/public void refreshSystemAllRescMap() {systemAllResourceMap = this.rescApi.getSystemAllRescMap(null).getResult();}@Pointcut("(execution(* com.coderman..controller..*(..)))")public void pointcut() {}@Around("pointcut()")public Object around(ProceedingJoinPoint point) throws Throwable {Cache<String, AuthUserVO> tokenCache = CacheUtil.getInstance().getTokenCache();Cache<Integer, String> deviceCache = CacheUtil.getInstance().getDeviceCache();HttpServletRequest request = HttpContextUtil.getHttpServletRequest();String path = request.getServletPath();// 白名单直接放行if (whiteListUrl.contains(path)) {return point.proceed();}// 访问令牌String token = AuthUtil.getToken();if (StringUtils.isBlank(token)) {throw new BusinessException(ResultConstant.RESULT_CODE_401, "会话已过期, 请重新登录");}// 系统不存在的资源直接返回if (!systemAllResourceMap.containsKey(path) && !unFilterHasLoginInfoUrl.contains(path)) {throw new BusinessException(ResultConstant.RESULT_CODE_404, "您访问的接口不存在!");}// 用户信息AuthUserVO authUserVO = null;try {authUserVO = tokenCache.get(token, () -> {log.debug("尝试从redis中获取用户信息结果.token:{}", token);return userApi.getUserByToken(token);});} catch (Exception ignore) {}if (authUserVO == null || System.currentTimeMillis() > authUserVO.getExpiredTime()) {tokenCache.invalidate(token);throw new BusinessException(ResultConstant.RESULT_CODE_401, "会话已过期, 请重新登录");}// 单设备校验if (isOneDeviceLogin) {Integer userId = authUserVO.getUserId();String deviceToken = StringUtils.EMPTY;try {deviceToken = deviceCache.get(userId, () -> {log.debug("尝试从redis中获取设备信息结果.userId:{}", userId);return userApi.getTokenByUserId(userId);});} catch (Exception ignore) {}if (StringUtils.isNotBlank(deviceToken) && !StringUtils.equals(deviceToken, token)) {deviceCache.invalidate(userId);throw new BusinessException(ResultConstant.RESULT_CODE_401, "账号已在其他设备上登录!");}}// 不需要过滤的url且有登入信息,设置会话后直接放行if (unFilterHasLoginInfoUrl.contains(path)) {AuthUtil.setCurrent(authUserVO);return point.proceed();}// 验证用户权限List<Integer> myRescIds = authUserVO.getRescIdList();Set<Integer> rescIds = Sets.newHashSet();if (CollectionUtils.isNotEmpty(systemAllResourceMap.get(path))) {rescIds = new HashSet<>(systemAllResourceMap.get(path));}if (CollectionUtils.isNotEmpty(myRescIds)) {for (Integer rescId : rescIds) {if (myRescIds.contains(rescId)) {AuthUtil.setCurrent(authUserVO);return point.proceed();}}}throw new BusinessException(ResultConstant.RESULT_CODE_403, "接口无权限");}@RedisChannelListener(channelName = RedisConstant.CHANNEL_REFRESH_RESC)public void refreshRescListener(String msgContent) {log.warn("doRefreshResc start - > {}", msgContent);this.refreshSystemAllRescMap();log.warn("doRefreshResc end - > {}", msgContent);}@RedisChannelListener(channelName = RedisConstant.CHANNEL_REFRESH_SESSION_CACHE, clazz = AuthUserVO.class)public void refreshSessionCache(AuthUserVO logoutUser) {String token = logoutUser.getAccessToken();Integer userId = logoutUser.getUserId();log.warn("doUserLogout start - > {}", token);// 清除会话缓存Cache<String, AuthUserVO> tokenCache = CacheUtil.getInstance().getTokenCache();tokenCache.invalidate(token);// 清除设备缓存Cache<Integer, String> deviceCache = CacheUtil.getInstance().getDeviceCache();deviceCache.invalidate(userId);log.warn("doUserLogout end - > {}", token);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/489000.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【架构】从 Socket 的角度认识非阻塞模型

文章目录 前言1. 阻塞模型2. 非阻塞模型2.1 Reactor 模型优势2.2 Reactor 模型劣势 后记 前言 近期看了很多中间件的文章&#xff0c;RocketMQ&#xff0c;Dubbo 这些中间件内部的rpc通信都用的是非阻塞的模型。(Netty)&#xff0c;这里从 Socket 的角度总结一下。 1. 阻塞模…

location和重定向、代理

location匹配的规则和优先级 在nginx当中&#xff0c;匹配的对象一般是URI来匹配 http://192.168.233.62/usr/local/nginx/html/index.html 182.168.233.61/ location匹配的分类&#xff1a; 多个location一旦匹配其中之一&#xff0c;不在匹配其他location 1、精确匹配 …

ragflow连ollama时出现的Bug

ragflow和ollama连接后&#xff0c;已经添加了两个模型但是ragflow仍然一直warn&#xff1a;Please add both embedding model and LLM in Settings &#xff1e; Model providers firstly.这里可能是我一开始拉取的镜像容器太小&#xff0c;容不下当前添加的模型&#xff0c;导…

软件测试面试问答

文章目录 什么是软件&#xff1f;软件测试工程师的工作内容什么是软件测试&#xff1f;软件开发生命周期软件开发的几个阶段软件bug的五个要素Bug的十大要素:软件测试的分类软件测试方法分类单元测试设计测试用例的主要方法什么是测试用例测试用例几大要素你的测试职业发展是什…

python学习笔记—7—变量拼接

1. 字符串的拼接 print(var_1 var_2) print("supercarry" "doinb") name "doinb" sex "man" score "100" print("sex:" sex " name:" name " score:" score) 注意&#xff1a; …

Win10环境vscode+latex+中文快速配置

安装vscodelatex workshop 配置&#xff1a; {"liveServer.settings.donotVerifyTags": true,"liveServer.settings.donotShowInfoMsg": true,"explorer.confirmDelete": false,"files.autoSave": "afterDelay","exp…

AI生成不了复杂前端页面?也许有解决方案了

在2024年&#xff0c;编程成为了人工智能领域最热门的赛道。AI编程技术正以惊人的速度进步&#xff0c;但在生成前端页面方面&#xff0c;AI的能力还是饱受质疑。自从ScriptEcho平台上线以来&#xff0c;我们收到了不少用户的反馈&#xff0c;他们表示&#xff1a;“生成的页面…

【热力学与工程流体力学】流体静力学实验,雷诺实验,沿程阻力实验,丘里流量计流量系数测定,局部阻力系数的测定,稳态平板法测定材料的导热系数λ

关注作者了解更多 我的其他CSDN专栏 过程控制系统 工程测试技术 虚拟仪器技术 可编程控制器 工业现场总线 数字图像处理 智能控制 传感器技术 嵌入式系统 复变函数与积分变换 单片机原理 线性代数 大学物理 热工与工程流体力学 数字信号处理 光电融合集成电路…

360极速浏览器不支持看PDF

360安全浏览器采用的是基于IE内核和Chrome内核的双核浏览器。360极速浏览器是源自Chromium开源项目的浏览器&#xff0c;不但完美融合了IE内核引擎&#xff0c;而且实现了双核引擎的无缝切换。因此在速度上&#xff0c;360极速浏览器的极速体验感更佳。 展示自己的时候要在有优…

【深度学习】深刻理解ViT

ViT&#xff08;Vision Transformer&#xff09;是谷歌研究团队于2020年提出的一种新型图像识别模型&#xff0c;首次将Transformer架构成功应用于计算机视觉任务中。Transformer最初应用于自然语言处理&#xff08;如BERT和GPT&#xff09;&#xff0c;而ViT展示了其在视觉任务…

idea 配置 git .gitignore文件配置

.gitignore 内容 .idea/ *.iml target/ *.class *.log .iml在idea项目里面创建一个.gitignore名字的文件&#xff0c;然后把这个文件提交到git上。我一般是放到.idea同级目录。 我遇到了几种情况这个文件配置了但是不生效的情况 第一种 Git的缓存可能会导致配置不生效。尝试…

Scala的隐式转换

&#xff08;一&#xff09;隐式转换&#xff1a; 编译器 偷偷地&#xff0c;自动地帮我们把一种数据类型转换为另一种类型。例如&#xff1a;int --> double 它有失败的时候(double-->int),有成功的时候。 当它转换失败的时候&#xff0c;我们提供一个工具&#xff0c;让…

Windows安装WSL子系统及docker,以及WSL和docker配置、使用及问题解决

在Windows操作系统中,Ubuntu子系统(也称为Windows Subsystem for Linux, WSL)为开发者提供了一个在Windows环境下运行Linux环境的平台。然而,有时用户在按照Ubuntu子系统或者使用WSL时,可能会遇到各种问题,下面总结一下解决方式。 想要在Windows上安装Docker(实际上是基…

F12抓包01:启动、面板功能介绍、语言设置、前端样式调试

浏览器检查工具通常用来作为浏览器web服务测试过程中&#xff0c;辅助测试、排查问题、定位缺陷的工具。 本文以mac系统下&#xff0c;当前比较常用的Chrome浏览器为例&#xff0c;讲解“检查”工具的常用功能操作方法。 一、打开方式 **1、****鼠标操作&#xff1a;**浏览器…

仿iOS日历、飞书日历、Google日历的日模式

仿iOS日历、飞书日历、Google日历的日模式&#xff0c;24H内事件可自由上下拖动、自由拉伸。 以下是效果图&#xff1a; 具体实现比较简单&#xff0c;代码如下&#xff1a; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color;…

特征交叉-CAN学习笔记代码解读

一 核心模块coaction 对于每个特征对(feature_pairs)weight, bias 来自于P_inductionP_fead是MLP的input 举个例子&#xff1a;如果是用户ID和产品ID的co-action&#xff0c;且产品ID是做induction&#xff0c;用户ID是做feed。 step1 用户ID/产品ID都先形成一个向量&#xf…

Java从入门到工作3 - 框架/工具

3.1、SpringBoot框架结构 在 Spring Boot 或微服务架构中&#xff0c;每个服务的文件目录结构通常遵循一定的约定。以下是一个常见的 Spring Boot 服务目录结构示例&#xff0c;以及各个文件和目录的简要说明&#xff1a; my-service │ ├── src │ ├── main │ │…

基于SpringBoot的青少年心理健康教育网站

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

基于事件驱动的websocket简单实现

websocket的实现 什么是websocket&#xff1f; WebSocket 是一种网络通信协议&#xff0c;旨在为客户端和服务器之间提供全双工、实时的通信通道。它是在 HTML5 规范中引入的&#xff0c;可以让浏览器与服务器进行持久化连接&#xff0c;以便实现低延迟的数据交换。 WebSock…

JavaEE 【知识改变命运】04 多线程(3)

文章目录 多线程带来的风险-线程安全线程不安全的举例分析产出线程安全的原因&#xff1a;1.线程是抢占式的2. 多线程修改同一个变量&#xff08;程序的要求&#xff09;3. 原子性4. 内存可见性5. 指令重排序 总结线程安全问题产生的原因解决线程安全问题1. synchronized关键字…