From 6830350411c0d76c9180eea807f6558d8f524e71 Mon Sep 17 00:00:00 2001 From: cwchen <1048842385@qq.com> Date: Thu, 25 Sep 2025 14:32:28 +0800 Subject: [PATCH] =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-dev.yml | 131 +++++++++++++++++ .../src/main/resources/application-prod.yml | 131 +++++++++++++++++ .../src/main/resources/application-test.yml | 131 +++++++++++++++++ .../src/main/resources/application.yml | 132 +---------------- .../resources/mybatis/application-prod.yml | 133 ++++++++++++++++++ .../bonus/common/constant/CacheConstants.java | 6 + .../common/core/domain/model/LoginUser.java | 13 ++ .../filter/JwtAuthenticationTokenFilter.java | 23 +++ .../handle/AuthenticationEntryPointImpl.java | 14 +- .../framework/web/service/TokenService.java | 88 ++++++++++++ pom.xml | 54 +++++++ 11 files changed, 724 insertions(+), 132 deletions(-) create mode 100644 bonus-admin/src/main/resources/application-dev.yml create mode 100644 bonus-admin/src/main/resources/application-prod.yml create mode 100644 bonus-admin/src/main/resources/application-test.yml create mode 100644 bonus-admin/src/main/resources/mybatis/application-prod.yml diff --git a/bonus-admin/src/main/resources/application-dev.yml b/bonus-admin/src/main/resources/application-dev.yml new file mode 100644 index 0000000..9e9c0af --- /dev/null +++ b/bonus-admin/src/main/resources/application-dev.yml @@ -0,0 +1,131 @@ +# 项目相关配置 +bonus: + # 名称 + name: bonus + # 版本 + version: 3.9.0 + # 版权年份 + copyrightYear: 2025 + # 文件路径 示例( Windows配置D:/bonus/uploadPath,Linux配置 /home/bonus/uploadPath) + profile: D:/bonus/uploadPath + # 获取ip地址开关 + addressEnabled: false + # 验证码类型 math 数字计算 char 字符验证 + captchaType: math + +sql: + filePath: E:/bonus/filePath + schemaName: smart_archives_dev + +# 开发环境配置 +server: + # 服务器的HTTP端口,默认为8080 + port: 8080 + servlet: + # 应用的访问路径 + context-path: /smartArchives + tomcat: + # tomcat的URI编码 + uri-encoding: UTF-8 + # 连接数满后的排队数,默认为100 + accept-count: 1000 + threads: + # tomcat最大线程数,默认为200 + max: 800 + # Tomcat启动初始化的线程数,默认值10 + min-spare: 100 + +# 日志配置 +logging: + level: + com.bonus: info + org.springframework: warn + +# 用户配置 +user: + password: + # 密码最大错误次数 + maxRetryCount: 5 + # 密码锁定时间(默认10分钟) + lockTime: 10 + +# Spring配置 +spring: + # 资源信息 + messages: + # 国际化资源文件路径 + basename: i18n/messages + # 文件上传 + servlet: + multipart: + # 单个文件大小 + max-file-size: 10MB + # 设置总上传的文件大小 + max-request-size: 20MB + # 服务模块 + devtools: + restart: + # 热部署开关 + enabled: true + # redis 配置 + redis: + # 地址 + host: localhost + # 端口,默认为6379 + port: 6379 + # 数据库索引 + database: 0 + # 密码 + password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池中的最小空闲连接 + min-idle: 0 + # 连接池中的最大空闲连接 + max-idle: 8 + # 连接池的最大数据库连接数 + max-active: 8 + # #连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + +# token配置 +token: + # 令牌自定义标识 + header: Authorization + # 令牌密钥 + secret: abcdefghijklmnopqrstuvwxyz + # 令牌有效期(默认30分钟) + expireTime: 30 + +# MyBatis配置 +mybatis: + # 搜索指定包别名 + typeAliasesPackage: com.bonus.**.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath*:mapper/**/*Mapper.xml + # 加载全局的配置文件 + configLocation: classpath:mybatis/mybatis-config.xml + +# PageHelper分页插件 +pagehelper: + helperDialect: mysql + supportMethodsArguments: true + params: count=countSql + +# Swagger配置 +swagger: + # 是否开启swagger + enabled: true + # 请求前缀 + pathMapping: /dev-api + +# 防止XSS攻击 +xss: + # 过滤开关 + enabled: true + # 排除链接(多个用逗号分隔) + excludes: /system/notice + # 匹配链接 + urlPatterns: /system/*,/monitor/*,/tool/* diff --git a/bonus-admin/src/main/resources/application-prod.yml b/bonus-admin/src/main/resources/application-prod.yml new file mode 100644 index 0000000..9e9c0af --- /dev/null +++ b/bonus-admin/src/main/resources/application-prod.yml @@ -0,0 +1,131 @@ +# 项目相关配置 +bonus: + # 名称 + name: bonus + # 版本 + version: 3.9.0 + # 版权年份 + copyrightYear: 2025 + # 文件路径 示例( Windows配置D:/bonus/uploadPath,Linux配置 /home/bonus/uploadPath) + profile: D:/bonus/uploadPath + # 获取ip地址开关 + addressEnabled: false + # 验证码类型 math 数字计算 char 字符验证 + captchaType: math + +sql: + filePath: E:/bonus/filePath + schemaName: smart_archives_dev + +# 开发环境配置 +server: + # 服务器的HTTP端口,默认为8080 + port: 8080 + servlet: + # 应用的访问路径 + context-path: /smartArchives + tomcat: + # tomcat的URI编码 + uri-encoding: UTF-8 + # 连接数满后的排队数,默认为100 + accept-count: 1000 + threads: + # tomcat最大线程数,默认为200 + max: 800 + # Tomcat启动初始化的线程数,默认值10 + min-spare: 100 + +# 日志配置 +logging: + level: + com.bonus: info + org.springframework: warn + +# 用户配置 +user: + password: + # 密码最大错误次数 + maxRetryCount: 5 + # 密码锁定时间(默认10分钟) + lockTime: 10 + +# Spring配置 +spring: + # 资源信息 + messages: + # 国际化资源文件路径 + basename: i18n/messages + # 文件上传 + servlet: + multipart: + # 单个文件大小 + max-file-size: 10MB + # 设置总上传的文件大小 + max-request-size: 20MB + # 服务模块 + devtools: + restart: + # 热部署开关 + enabled: true + # redis 配置 + redis: + # 地址 + host: localhost + # 端口,默认为6379 + port: 6379 + # 数据库索引 + database: 0 + # 密码 + password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池中的最小空闲连接 + min-idle: 0 + # 连接池中的最大空闲连接 + max-idle: 8 + # 连接池的最大数据库连接数 + max-active: 8 + # #连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + +# token配置 +token: + # 令牌自定义标识 + header: Authorization + # 令牌密钥 + secret: abcdefghijklmnopqrstuvwxyz + # 令牌有效期(默认30分钟) + expireTime: 30 + +# MyBatis配置 +mybatis: + # 搜索指定包别名 + typeAliasesPackage: com.bonus.**.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath*:mapper/**/*Mapper.xml + # 加载全局的配置文件 + configLocation: classpath:mybatis/mybatis-config.xml + +# PageHelper分页插件 +pagehelper: + helperDialect: mysql + supportMethodsArguments: true + params: count=countSql + +# Swagger配置 +swagger: + # 是否开启swagger + enabled: true + # 请求前缀 + pathMapping: /dev-api + +# 防止XSS攻击 +xss: + # 过滤开关 + enabled: true + # 排除链接(多个用逗号分隔) + excludes: /system/notice + # 匹配链接 + urlPatterns: /system/*,/monitor/*,/tool/* diff --git a/bonus-admin/src/main/resources/application-test.yml b/bonus-admin/src/main/resources/application-test.yml new file mode 100644 index 0000000..5d4f444 --- /dev/null +++ b/bonus-admin/src/main/resources/application-test.yml @@ -0,0 +1,131 @@ +# 项目相关配置 +bonus: + # 名称 + name: bonus + # 版本 + version: 3.9.0 + # 版权年份 + copyrightYear: 2025 + # 文件路径 示例( Windows配置D:/bonus/uploadPath,Linux配置 /home/bonus/uploadPath) + profile: /home/bonus/uploadPath + # 获取ip地址开关 + addressEnabled: false + # 验证码类型 math 数字计算 char 字符验证 + captchaType: math + +sql: + filePath: /home/bonus/filePath + schemaName: smart_archives_dev + +# 开发环境配置 +server: + # 服务器的HTTP端口,默认为8080 + port: 39080 + servlet: + # 应用的访问路径 + context-path: /smartArchives + tomcat: + # tomcat的URI编码 + uri-encoding: UTF-8 + # 连接数满后的排队数,默认为100 + accept-count: 1000 + threads: + # tomcat最大线程数,默认为200 + max: 800 + # Tomcat启动初始化的线程数,默认值10 + min-spare: 100 + +# 日志配置 +logging: + level: + com.bonus: info + org.springframework: warn + +# 用户配置 +user: + password: + # 密码最大错误次数 + maxRetryCount: 5 + # 密码锁定时间(默认10分钟) + lockTime: 10 + +# Spring配置 +spring: + # 资源信息 + messages: + # 国际化资源文件路径 + basename: i18n/messages + # 文件上传 + servlet: + multipart: + # 单个文件大小 + max-file-size: 10MB + # 设置总上传的文件大小 + max-request-size: 20MB + # 服务模块 + devtools: + restart: + # 热部署开关 + enabled: true + # redis 配置 + redis: + # 地址 + host: 192.168.0.14 + # 端口,默认为6379 + port: 2004 + # 数据库索引 + database: 6 + # 密码 + password: Plzbns@Redis123! + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池中的最小空闲连接 + min-idle: 0 + # 连接池中的最大空闲连接 + max-idle: 8 + # 连接池的最大数据库连接数 + max-active: 8 + # #连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + +# token配置 +token: + # 令牌自定义标识 + header: Authorization + # 令牌密钥 + secret: abcdefghijklmnopqrstuvwxyz + # 令牌有效期(默认30分钟) + expireTime: 30 + +# MyBatis配置 +mybatis: + # 搜索指定包别名 + typeAliasesPackage: com.bonus.**.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath*:mapper/**/*Mapper.xml + # 加载全局的配置文件 + configLocation: classpath:mybatis/mybatis-config.xml + +# PageHelper分页插件 +pagehelper: + helperDialect: mysql + supportMethodsArguments: true + params: count=countSql + +# Swagger配置 +swagger: + # 是否开启swagger + enabled: true + # 请求前缀 + pathMapping: /dev-api + +# 防止XSS攻击 +xss: + # 过滤开关 + enabled: true + # 排除链接(多个用逗号分隔) + excludes: /system/notice + # 匹配链接 + urlPatterns: /system/*,/monitor/*,/tool/* diff --git a/bonus-admin/src/main/resources/application.yml b/bonus-admin/src/main/resources/application.yml index 71339f9..ecdcd82 100644 --- a/bonus-admin/src/main/resources/application.yml +++ b/bonus-admin/src/main/resources/application.yml @@ -1,133 +1,3 @@ -# 项目相关配置 -bonus: - # 名称 - name: bonus - # 版本 - version: 3.9.0 - # 版权年份 - copyrightYear: 2025 - # 文件路径 示例( Windows配置D:/bonus/uploadPath,Linux配置 /home/bonus/uploadPath) - profile: E:/bonus/uploadPath - # 获取ip地址开关 - addressEnabled: false - # 验证码类型 math 数字计算 char 字符验证 - captchaType: math - -sql: - filePath: E:/bonus/filePath - schemaName: smart_archives_dev - -# 开发环境配置 -server: - # 服务器的HTTP端口,默认为8080 - port: 8080 - servlet: - # 应用的访问路径 - context-path: /smartArchives - tomcat: - # tomcat的URI编码 - uri-encoding: UTF-8 - # 连接数满后的排队数,默认为100 - accept-count: 1000 - threads: - # tomcat最大线程数,默认为200 - max: 800 - # Tomcat启动初始化的线程数,默认值10 - min-spare: 100 - -# 日志配置 -logging: - level: - com.bonus: debug - org.springframework: warn - -# 用户配置 -user: - password: - # 密码最大错误次数 - maxRetryCount: 5 - # 密码锁定时间(默认10分钟) - lockTime: 10 - -# Spring配置 spring: - # 资源信息 - messages: - # 国际化资源文件路径 - basename: i18n/messages profiles: - active: druid - # 文件上传 - servlet: - multipart: - # 单个文件大小 - max-file-size: 10MB - # 设置总上传的文件大小 - max-request-size: 20MB - # 服务模块 - devtools: - restart: - # 热部署开关 - enabled: true - # redis 配置 - redis: - # 地址 - host: localhost - # 端口,默认为6379 - port: 6379 - # 数据库索引 - database: 0 - # 密码 - password: - # 连接超时时间 - timeout: 10s - lettuce: - pool: - # 连接池中的最小空闲连接 - min-idle: 0 - # 连接池中的最大空闲连接 - max-idle: 8 - # 连接池的最大数据库连接数 - max-active: 8 - # #连接池最大阻塞等待时间(使用负值表示没有限制) - max-wait: -1ms - -# token配置 -token: - # 令牌自定义标识 - header: Authorization - # 令牌密钥 - secret: abcdefghijklmnopqrstuvwxyz - # 令牌有效期(默认30分钟) - expireTime: 30 - -# MyBatis配置 -mybatis: - # 搜索指定包别名 - typeAliasesPackage: com.bonus.**.domain - # 配置mapper的扫描,找到所有的mapper.xml映射文件 - mapperLocations: classpath*:mapper/**/*Mapper.xml - # 加载全局的配置文件 - configLocation: classpath:mybatis/mybatis-config.xml - -# PageHelper分页插件 -pagehelper: - helperDialect: mysql - supportMethodsArguments: true - params: count=countSql - -# Swagger配置 -swagger: - # 是否开启swagger - enabled: true - # 请求前缀 - pathMapping: /dev-api - -# 防止XSS攻击 -xss: - # 过滤开关 - enabled: true - # 排除链接(多个用逗号分隔) - excludes: /system/notice - # 匹配链接 - urlPatterns: /system/*,/monitor/*,/tool/* + active: @profiles.active@,druid \ No newline at end of file diff --git a/bonus-admin/src/main/resources/mybatis/application-prod.yml b/bonus-admin/src/main/resources/mybatis/application-prod.yml new file mode 100644 index 0000000..afc41f7 --- /dev/null +++ b/bonus-admin/src/main/resources/mybatis/application-prod.yml @@ -0,0 +1,133 @@ +# 项目相关配置 +bonus: + # 名称 + name: bonus + # 版本 + version: 3.9.0 + # 版权年份 + copyrightYear: 2025 + # 文件路径 示例( Windows配置D:/bonus/uploadPath,Linux配置 /home/bonus/uploadPath) + profile: D:/bonus/uploadPath + # 获取ip地址开关 + addressEnabled: false + # 验证码类型 math 数字计算 char 字符验证 + captchaType: math + +sql: + filePath: E:/bonus/filePath + schemaName: smart_archives_dev + +# 开发环境配置 +server: + # 服务器的HTTP端口,默认为8080 + port: 8080 + servlet: + # 应用的访问路径 + context-path: /smartArchives + tomcat: + # tomcat的URI编码 + uri-encoding: UTF-8 + # 连接数满后的排队数,默认为100 + accept-count: 1000 + threads: + # tomcat最大线程数,默认为200 + max: 800 + # Tomcat启动初始化的线程数,默认值10 + min-spare: 100 + +# 日志配置 +logging: + level: + com.bonus: info + org.springframework: warn + +# 用户配置 +user: + password: + # 密码最大错误次数 + maxRetryCount: 5 + # 密码锁定时间(默认10分钟) + lockTime: 10 + +# Spring配置 +spring: + # 资源信息 + messages: + # 国际化资源文件路径 + basename: i18n/messages + profiles: + active: druid + # 文件上传 + servlet: + multipart: + # 单个文件大小 + max-file-size: 10MB + # 设置总上传的文件大小 + max-request-size: 20MB + # 服务模块 + devtools: + restart: + # 热部署开关 + enabled: true + # redis 配置 + redis: + # 地址 + host: localhost + # 端口,默认为6379 + port: 6379 + # 数据库索引 + database: 0 + # 密码 + password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池中的最小空闲连接 + min-idle: 0 + # 连接池中的最大空闲连接 + max-idle: 8 + # 连接池的最大数据库连接数 + max-active: 8 + # #连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + +# token配置 +token: + # 令牌自定义标识 + header: Authorization + # 令牌密钥 + secret: abcdefghijklmnopqrstuvwxyz + # 令牌有效期(默认30分钟) + expireTime: 30 + +# MyBatis配置 +mybatis: + # 搜索指定包别名 + typeAliasesPackage: com.bonus.**.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath*:mapper/**/*Mapper.xml + # 加载全局的配置文件 + configLocation: classpath:mybatis/mybatis-config.xml + +# PageHelper分页插件 +pagehelper: + helperDialect: mysql + supportMethodsArguments: true + params: count=countSql + +# Swagger配置 +swagger: + # 是否开启swagger + enabled: true + # 请求前缀 + pathMapping: /dev-api + +# 防止XSS攻击 +xss: + # 过滤开关 + enabled: true + # 排除链接(多个用逗号分隔) + excludes: /system/notice + # 匹配链接 + urlPatterns: /system/*,/monitor/*,/tool/* diff --git a/bonus-common/src/main/java/com/bonus/common/constant/CacheConstants.java b/bonus-common/src/main/java/com/bonus/common/constant/CacheConstants.java index 5d3f73c..cb76caa 100644 --- a/bonus-common/src/main/java/com/bonus/common/constant/CacheConstants.java +++ b/bonus-common/src/main/java/com/bonus/common/constant/CacheConstants.java @@ -12,6 +12,12 @@ public class CacheConstants */ public static final String LOGIN_TOKEN_KEY = "login_tokens:"; + /** + * 用户与token绑定关系 redis key(限制单端在线) + * 格式:user_tokens:{username} -> {token-uuid} + */ + public static final String USER_TOKEN_KEY = "user_tokens:"; + /** * 验证码 redis key */ diff --git a/bonus-common/src/main/java/com/bonus/common/core/domain/model/LoginUser.java b/bonus-common/src/main/java/com/bonus/common/core/domain/model/LoginUser.java index bf2e63d..847399d 100644 --- a/bonus-common/src/main/java/com/bonus/common/core/domain/model/LoginUser.java +++ b/bonus-common/src/main/java/com/bonus/common/core/domain/model/LoginUser.java @@ -21,6 +21,11 @@ public class LoginUser implements UserDetails */ private Long userId; + /** + * 是否被其他设备强制下线 + */ + private boolean forceLogoutByOtherDevice = false; + /** * 部门ID */ @@ -258,6 +263,14 @@ public class LoginUser implements UserDetails this.user = user; } + public boolean isForceLogoutByOtherDevice() { + return forceLogoutByOtherDevice; + } + + public void setForceLogoutByOtherDevice(boolean forceLogoutByOtherDevice) { + this.forceLogoutByOtherDevice = forceLogoutByOtherDevice; + } + @Override public Collection getAuthorities() { diff --git a/bonus-framework/src/main/java/com/bonus/framework/security/filter/JwtAuthenticationTokenFilter.java b/bonus-framework/src/main/java/com/bonus/framework/security/filter/JwtAuthenticationTokenFilter.java index 6d37d73..929d251 100644 --- a/bonus-framework/src/main/java/com/bonus/framework/security/filter/JwtAuthenticationTokenFilter.java +++ b/bonus-framework/src/main/java/com/bonus/framework/security/filter/JwtAuthenticationTokenFilter.java @@ -5,6 +5,10 @@ import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import com.alibaba.fastjson2.JSON; +import com.bonus.common.constant.HttpStatus; +import com.bonus.common.core.domain.AjaxResult; +import com.bonus.common.utils.ServletUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -34,6 +38,25 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter throws ServletException, IOException { LoginUser loginUser = tokenService.getLoginUser(request); + // 若检测到被其他设备挤下线,直接返回特定提示 + Object forced = request.getAttribute("forceLogoutByOtherDevice"); + if (Boolean.TRUE.equals(forced)) + { + String msg = "检测到您的账号已在其他设备登录"; + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.UNAUTHORIZED, msg))); + return; + } + if (StringUtils.isNotNull(loginUser)) + { + // 如果是被其他设备挤下线的标记对象,则不注入认证,交由未认证处理器返回前端提示 + try { + if (loginUser.isForceLogoutByOtherDevice()) + { + chain.doFilter(request, response); + return; + } + } catch (Exception ignore) { } + } if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) { tokenService.verifyToken(loginUser); diff --git a/bonus-framework/src/main/java/com/bonus/framework/security/handle/AuthenticationEntryPointImpl.java b/bonus-framework/src/main/java/com/bonus/framework/security/handle/AuthenticationEntryPointImpl.java index 8b03bf6..826874b 100644 --- a/bonus-framework/src/main/java/com/bonus/framework/security/handle/AuthenticationEntryPointImpl.java +++ b/bonus-framework/src/main/java/com/bonus/framework/security/handle/AuthenticationEntryPointImpl.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.Serializable; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; + import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; @@ -23,12 +24,23 @@ public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, S { private static final long serialVersionUID = -8970718410437077606L; + // 通过 request attribute 判断是否是多设备登录挤下线 + @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException { int code = HttpStatus.UNAUTHORIZED; - String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); + String msg; + Object forced = request.getAttribute("forceLogoutByOtherDevice"); + if (Boolean.TRUE.equals(forced)) + { + msg = "检测到您的账号已在其他设备登录,当前会话已下线。如非本人操作,请立即修改密码。"; + } + else + { + msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); + } ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); } } diff --git a/bonus-framework/src/main/java/com/bonus/framework/web/service/TokenService.java b/bonus-framework/src/main/java/com/bonus/framework/web/service/TokenService.java index 79124c5..9cd4f4c 100644 --- a/bonus-framework/src/main/java/com/bonus/framework/web/service/TokenService.java +++ b/bonus-framework/src/main/java/com/bonus/framework/web/service/TokenService.java @@ -72,8 +72,28 @@ public class TokenService Claims claims = parseToken(token); // 解析对应的权限以及用户信息 String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + String usernameFromJwt = (String) claims.get(Constants.JWT_USERNAME); String userKey = getTokenKey(uuid); LoginUser user = redisCache.getCacheObject(userKey); + if (StringUtils.isNull(user)) + { + // login_tokens:{uuid} 已不存在:视为会话被挤下线或已失效 + // 为确保前端能提示“其他设备登录”,在能解析出用户名时一律标记 + if (StringUtils.isNotEmpty(usernameFromJwt)) + { + request.setAttribute("forceLogoutByOtherDevice", Boolean.TRUE); + } + return null; + } + // 单端在线校验:username -> uuid 映射需要与当前token匹配 + String username = user.getUsername(); + String mappedUuid = redisCache.getCacheObject(getUserTokenKey(username)); + if (StringUtils.isEmpty(mappedUuid) || !uuid.equals(mappedUuid)) + { + // 当前token已被挤下线或无效,标记前端可识别的提示 + request.setAttribute("forceLogoutByOtherDevice", Boolean.TRUE); + return null; + } return user; } catch (Exception e) @@ -103,7 +123,20 @@ public class TokenService if (StringUtils.isNotEmpty(token)) { String userKey = getTokenKey(token); + LoginUser loginUser = redisCache.getCacheObject(userKey); + // 删除loginUser缓存 redisCache.deleteObject(userKey); + // 同步删除用户名->token映射(仅当映射值等于当前token时) + if (StringUtils.isNotNull(loginUser)) + { + String username = loginUser.getUsername(); + String userTokenKey = getUserTokenKey(username); + String mappedUuid = redisCache.getCacheObject(userTokenKey); + if (token.equals(mappedUuid)) + { + redisCache.deleteObject(userTokenKey); + } + } } } @@ -118,7 +151,10 @@ public class TokenService String token = IdUtils.fastUUID(); loginUser.setToken(token); setUserAgent(loginUser); + // 刷新并写入 login_tokens:{uuid} -> LoginUser refreshToken(loginUser); + // 绑定用户名与当前uuid,挤下线旧会话 + bindUserToken(loginUser.getUsername(), token); Map claims = new HashMap<>(); claims.put(Constants.LOGIN_USER_KEY, token); @@ -154,6 +190,14 @@ public class TokenService // 根据uuid将loginUser缓存 String userKey = getTokenKey(loginUser.getToken()); redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); + // 同步刷新用户名->token映射的过期时间,避免映射过期时间短导致误下线 + String username = loginUser.getUsername(); + String userTokenKey = getUserTokenKey(username); + String mappedUuid = redisCache.getCacheObject(userTokenKey); + if (StringUtils.isNotEmpty(mappedUuid) && mappedUuid.equals(loginUser.getToken())) + { + redisCache.expire(userTokenKey, expireTime, TimeUnit.MINUTES); + } } /** @@ -237,6 +281,14 @@ public class TokenService return CacheConstants.LOGIN_TOKEN_KEY + uuid; } + /** + * 获取 用户名->token 映射的key + */ + private String getUserTokenKey(String username) + { + return CacheConstants.USER_TOKEN_KEY + username; + } + /** * 获取请求token */ @@ -252,8 +304,28 @@ public class TokenService Claims claims = parseToken(token); // 解析对应的权限以及用户信息 String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + String usernameFromJwt = (String) claims.get(Constants.JWT_USERNAME); String userKey = getTokenKey(uuid); LoginUser user = redisCache.getCacheObject(userKey); + if (StringUtils.isNull(user)) + { + // 若login_tokens无此uuid,检查用户名映射,若映射存在且指向其他uuid,则视为被其他设备挤下线 + if (StringUtils.isNotEmpty(usernameFromJwt)) + { + String mappedUuid = redisCache.getCacheObject(getUserTokenKey(usernameFromJwt)); + if (StringUtils.isNotEmpty(mappedUuid) && !uuid.equals(mappedUuid)) + { + return null; + } + } + return null; + } + String username = user.getUsername(); + String mappedUuid = redisCache.getCacheObject(getUserTokenKey(username)); + if (StringUtils.isEmpty(mappedUuid) || !uuid.equals(mappedUuid)) + { + return null; + } return user; } catch (Exception e) @@ -263,4 +335,20 @@ public class TokenService } return null; } + + /** + * 绑定用户与token(uuid),并挤下线旧会话 + */ + private void bindUserToken(String username, String uuid) + { + String userTokenKey = getUserTokenKey(username); + String oldUuid = redisCache.getCacheObject(userTokenKey); + if (StringUtils.isNotEmpty(oldUuid) && !uuid.equals(oldUuid)) + { + // 删除旧的 login_tokens:{oldUuid} + String oldLoginKey = getTokenKey(oldUuid); + redisCache.deleteObject(oldLoginKey); + } + redisCache.setCacheObject(userTokenKey, uuid, expireTime, TimeUnit.MINUTES); + } } diff --git a/pom.xml b/pom.xml index 6eda87e..64eeac9 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,7 @@ 1.2.13 5.7.12 5.3.39 + test @@ -246,6 +247,25 @@ + + + src/main/resources + true + + **/application.yml + **/application.yaml + + + + + src/main/resources + false + + **/application.yml + **/application.yaml + + + @@ -273,4 +293,38 @@ + + + + + + dev + + dev + + + + + + + test + + test + + + true + + + + + + prod + + prod + + + + \ No newline at end of file