From a53dae3d7673d884f0fefa253d941f2c6fd937a3 Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Sun, 12 Jul 2026 12:32:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(iam):=20=E4=BC=9A=E8=AF=9D=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=94=AF=E6=8C=81=E5=B9=B6=E5=8F=91=E8=AE=A1=E6=95=B0?= =?UTF-8?q?=E8=8C=83=E5=9B=B4=E4=B8=8E=E7=BD=91=E5=85=B3=E8=B1=81=E5=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增concurrentScope(GLOBAL全局共享/PER_DEVICE按终端独立); GATEWAY终端豁免并发限制; 显式setIsShare(false)使代码自洽; DB默认值合理化新系统开箱即用; 精简yml删与框架默认一致项并修正注释 --- .../iam/auth/service/TokenService.java | 85 ++++++++++++++----- .../PlatformSessionManagementConfig.java | 2 + .../PlatformSessionManagementConfigParam.java | 3 + ...PlatformSessionManagementConfigResult.java | 3 + .../PlatformSecurityConfigService.java | 13 ++- .../src/main/resources/application.yml | 11 +-- 6 files changed, 85 insertions(+), 32 deletions(-) diff --git a/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/auth/service/TokenService.java b/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/auth/service/TokenService.java index 3a44eb2c7..e431c19d2 100644 --- a/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/auth/service/TokenService.java +++ b/daxpay-platform/daxpay-platform-service/service-iam/src/main/java/cn/daxpay/open/platform/iam/auth/service/TokenService.java @@ -23,6 +23,7 @@ import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformSe import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.stp.parameter.enums.SaLogoutMode; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotNull; @@ -197,25 +198,38 @@ public class TokenService { /// 执行 Sa-Token 登录(建立会话) private void doSaLogin(AuthInfoResult authInfoResult, String clientCode, String loginType) { + Object userId = authInfoResult.getId(); var saLoginModel = new SaLoginParameter() .setDeviceType(clientCode) - .setIsLastingCookie(true); - // 应用会话管理配置(在线时长/活跃超时/并发策略) - this.applySessionConfig(saLoginModel, authInfoResult.getId()); + .setIsLastingCookie(true) + // 逐设备隔离, 不依赖全局配置(代码自洽) + .setIsShare(false); + + var config = iamSecurityConfigService.getSessionManagement(); + // 应用在线时长/活跃超时(对所有终端生效) + this.applyTimeConfig(saLoginModel, config); + // 应用并发控制(GATEWAY豁免), 返回是否需要login后按终端踢人 + boolean perDeviceKickOldest = this.applyConcurrentConfig(saLoginModel, config, userId, clientCode); authInfoResult.setClient(clientCode) .setLoginType(loginType); - StpUtil.login(authInfoResult.getId(), saLoginModel); + StpUtil.login(userId, saLoginModel); + + // PER_DEVICE + KICK_OLDEST 必须在 login 后执行(login前本终端尚无token) + if (perDeviceKickOldest && config.getMaxConcurrentSessions() != null) { + StpUtil.stpLogic.logoutByMaxLoginCount( + userId, null, clientCode, + config.getMaxConcurrentSessions(), SaLogoutMode.KICKOUT); + } + SaSession session = StpUtil.getSession(); UserDetail userDetail = authInfoResult.getUserDetail(); session.set(CommonCode.USER, userDetail); } - /// 应用会话管理配置到 Sa-Token 登录参数 - /// 配置未启用时跳过, 继续使用 application.yml 中的静态默认值 - private void applySessionConfig(SaLoginParameter model, Object userId) { - PlatformSessionManagementConfig config = iamSecurityConfigService.getSessionManagement(); - if (config == null || !Boolean.TRUE.equals(config.getEnabled())) { + /// 应用在线时长与活跃超时配置 + private void applyTimeConfig(SaLoginParameter model, PlatformSessionManagementConfig config) { + if (config == null) { return; } // 在线时长 -> token 固定有效期(秒) @@ -226,26 +240,53 @@ public class TokenService { if (config.getActiveTimeoutHours() != null && config.getActiveTimeoutHours() > 0) { model.setActiveTimeout(config.getActiveTimeoutHours() * 3600L); } - // 并发登录策略 + } + + /// 应用并发登录控制 + /// 返回 true 表示需要在 login 后手动按终端踢最早会话(PER_DEVICE + KICK_OLDEST 场景) + private boolean applyConcurrentConfig(SaLoginParameter model, PlatformSessionManagementConfig config, + Object userId, String clientCode) { + if (config == null || !Boolean.TRUE.equals(config.getEnabled())) { + return false; + } + // GATEWAY 终端豁免: 网关为机器API调用, 不受并发限制 + if (ClientEnum.GATEWAY.getCode().equals(clientCode)) { + return false; + } Integer max = config.getMaxConcurrentSessions(); String strategy = config.getConcurrentStrategy(); - if ("KICK_OLDEST".equals(strategy) && max != null && max > 0) { - // 允许并发, 超出上限时 Sa-Token 自动注销最早的会话 - model.setIsConcurrent(true).setMaxLoginCount(max); + boolean perDevice = "PER_DEVICE".equals(config.getConcurrentScope()); + + // NEW_SESSION 或未配并发数: 允许并发, 不限制 + if (max == null || max <= 0 || "NEW_SESSION".equals(strategy)) { + model.setIsConcurrent(true); + return false; } - else if ("DENY_NEW".equals(strategy) && max != null && max > 0) { - // 登录前预检: 已达上限则拒绝新登录 - int current = StpUtil.getTokenValueListByLoginId(userId).size(); - if (current >= max) { + if ("KICK_OLDEST".equals(strategy)) { + model.setIsConcurrent(true); + if (perDevice) { + // PER_DEVICE: setMaxLoginCount是全局的, 需login后手动按deviceType踢 + return true; + } + // GLOBAL: Sa-Token login内部自动注销超额最早会话 + model.setMaxLoginCount(max) + .setOverflowLogoutMode(SaLogoutMode.KICKOUT); + return false; + } + if ("DENY_NEW".equals(strategy)) { + // 登录前预检: PER_DEVICE按当前终端计数, GLOBAL按所有终端总和 + List tokens = perDevice + ? StpUtil.getTokenValueListByLoginId(userId, clientCode) + : StpUtil.getTokenValueListByLoginId(userId); + if (tokens.size() >= max) { throw new LoginFailureException("error.auth.concurrentLimitExceeded"); } - // 不设 maxLoginCount, 保持"拒绝"语义, 避免触发 Sa-Token 自动踢旧 - model.setIsConcurrent(true); - } - else { - // NEW_SESSION 或未配置策略: 允许并发, 不限制数量 model.setIsConcurrent(true); + return false; } + // 未知策略: 允许并发, 不限制 + model.setIsConcurrent(true); + return false; } /// 退出 diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/security/PlatformSessionManagementConfig.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/security/PlatformSessionManagementConfig.java index ddec966c0..0c3c7f2ae 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/security/PlatformSessionManagementConfig.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/entity/config/platform/security/PlatformSessionManagementConfig.java @@ -19,4 +19,6 @@ public class PlatformSessionManagementConfig { private Integer maxConcurrentSessions; /// 并发会话策略 private String concurrentStrategy; + /// 并发计数范围(GLOBAL全局共享 / PER_DEVICE按终端独立), 默认GLOBAL + private String concurrentScope; } diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/security/PlatformSessionManagementConfigParam.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/security/PlatformSessionManagementConfigParam.java index e78198045..9ece4c950 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/security/PlatformSessionManagementConfigParam.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/param/config/security/PlatformSessionManagementConfigParam.java @@ -25,4 +25,7 @@ public class PlatformSessionManagementConfigParam { @Schema(description = "并发会话策略") private String concurrentStrategy; + + @Schema(description = "并发计数范围(GLOBAL全局共享 / PER_DEVICE按终端独立)") + private String concurrentScope; } diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/security/PlatformSessionManagementConfigResult.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/security/PlatformSessionManagementConfigResult.java index f88f9d9ac..c70683f80 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/security/PlatformSessionManagementConfigResult.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/result/config/security/PlatformSessionManagementConfigResult.java @@ -28,4 +28,7 @@ public class PlatformSessionManagementConfigResult extends BaseResult { @Schema(description = "并发会话策略") private String concurrentStrategy; + + @Schema(description = "并发计数范围(GLOBAL全局共享 / PER_DEVICE按终端独立)") + private String concurrentScope; } diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/security/PlatformSecurityConfigService.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/security/PlatformSecurityConfigService.java index ff8b1889c..d036f53df 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/security/PlatformSecurityConfigService.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/service/config/security/PlatformSecurityConfigService.java @@ -62,7 +62,18 @@ public class PlatformSecurityConfigService { public PlatformSessionManagementConfig getSessionManagementConfig() { return systemConfigService.getOrCreateConfig(PlatformConfigTypeEnum.SECURITY_SESSION, PlatformSessionManagementConfig.class, - new PlatformSessionManagementConfig()); + defaultSessionConfig()); + } + + /// 会话管理配置默认值: 新系统开箱即用 + private PlatformSessionManagementConfig defaultSessionConfig() { + return new PlatformSessionManagementConfig() + .setEnabled(true) + .setMaxOnlineHours(72) + .setActiveTimeoutHours(24) + .setMaxConcurrentSessions(5) + .setConcurrentStrategy("KICK_OLDEST") + .setConcurrentScope("GLOBAL"); } /// 获取会话管理配置 diff --git a/daxpay-start/src/main/resources/application.yml b/daxpay-start/src/main/resources/application.yml index 8cbd8cbf5..eb6924796 100644 --- a/daxpay-start/src/main/resources/application.yml +++ b/daxpay-start/src/main/resources/application.yml @@ -41,16 +41,9 @@ mybatis-plus-join: banner: false # 安全框架 sa-token: - # token 名称(同时也是 cookie 名称) + # token 名称(同时是 cookie/请求头名), 前端据此读写, 不可变更 token-name: Accesstoken - # token 有效期(单位:秒) 默认72小时,-1 代表永久有效 + # token 有效期兜底(秒), 仅在会话管理未启用时生效; 启用后以数据库配置 maxOnlineHours 为准 timeout: 259200 - # token 最低活跃频率(单位:秒) 默认24小时,-1 代表永久有效 - active-timeout: -1 - # 是否允许同一账号多地同时登 - is-concurrent: true - # 在多人登录同一账号时,是否共用一个 token - # false: 每次登录生成独立 token, 实现逐设备会话隔离(改密踢人时可精确保留当前 token) - is-share: false is-log: false is-print: false