From 34c607606803bbc61ce055cde2c57c3d9502c5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Fri, 24 Jul 2026 18:49:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E6=94=AF=E6=8C=81=E5=A4=9A?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E8=B6=85=E7=BA=A7=E7=AE=A1=E7=90=86=E5=91=98?= =?UTF-8?q?=E6=89=8B=E6=9C=BA=E5=8F=B7=E7=99=BB=E5=BD=95=E5=8F=8A=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E9=80=89=E6=8B=A9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改登录接口,支持手机号关联多个超级管理员实现多租户场景 - 多租户登录时返回租户列表供前端选择,不返回 access_token - 新增接口 loginBySelectTenant 选择租户后登录,生成并返回 access_token - 新增 TenantOption 类,封装租户选项信息 - UserMapper 新增跨租户查询超级管理员方法,忽略租户隔离 - UserService 增加对应多租户查询方法并实现 - 调整 SecurityConfig,添加多租户选择登录接口白名单 - LoginResult 新增租户列表字段及构造方法,适配多租户登录返回结果 --- .../common/core/security/SecurityConfig.java | 1 + .../system/controller/MainController.java | 102 +++++++++++++++--- .../common/system/mapper/UserMapper.java | 21 ++++ .../common/system/mapper/xml/UserMapper.xml | 29 +++++ .../common/system/result/LoginResult.java | 16 ++- .../common/system/result/TenantOption.java | 52 +++++++++ .../common/system/service/UserService.java | 19 ++++ .../system/service/impl/UserServiceImpl.java | 16 +++ 8 files changed, 240 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/gxwebsoft/common/system/result/TenantOption.java diff --git a/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java b/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java index f28a1ed..91fbc5a 100644 --- a/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java +++ b/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java @@ -58,6 +58,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { "/api/sendEmailCaptcha", "/api/loginBySms", "/api/loginBySuperAdminSms", + "/api/loginBySelectTenant", "/api/loginByDeveloperSms", "/api/system/user/regByPhone", "/api/parseToken/*", diff --git a/src/main/java/com/gxwebsoft/common/system/controller/MainController.java b/src/main/java/com/gxwebsoft/common/system/controller/MainController.java index f41a7a5..06c816c 100644 --- a/src/main/java/com/gxwebsoft/common/system/controller/MainController.java +++ b/src/main/java/com/gxwebsoft/common/system/controller/MainController.java @@ -41,6 +41,7 @@ import com.gxwebsoft.common.system.param.UpdatePasswordParam; import com.gxwebsoft.common.system.param.UserParam; import com.gxwebsoft.common.system.result.CaptchaResult; import com.gxwebsoft.common.system.result.LoginResult; +import com.gxwebsoft.common.system.result.TenantOption; import com.gxwebsoft.common.system.result.AccountInfoResult; import com.gxwebsoft.common.system.result.CheckPhoneResult; import com.gxwebsoft.common.system.service.*; @@ -64,6 +65,7 @@ import java.time.Instant; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; +import java.util.stream.Collectors; import java.util.concurrent.TimeUnit; import java.util.concurrent.ThreadLocalRandom; @@ -964,37 +966,111 @@ public class MainController extends BaseController { return fail(message, null); } - User user = userService.getLastLoginSuperAdminByPhone(phone); - if (user == null) { + // 查询该手机号关联的所有超级管理员记录(跨租户) + List superAdmins = userService.getSuperAdminsByPhone(phone); + if (superAdmins == null || superAdmins.isEmpty()) { String message = "用户不存在"; loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, null, request); return fail(message, null); } + + // 单租户:直接登录(现有逻辑不变,单租户用户无感知) + if (superAdmins.size() == 1) { + User user = superAdmins.get(0); + if (!Boolean.TRUE.equals(user.getIsSuperAdmin())) { + String message = "非超级管理员账号不允许登录"; + loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, user.getTenantId(), request); + return fail(message, null); + } + if (!user.getStatus().equals(0)) { + String message = "账号被冻结"; + loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_ERROR, message, user.getTenantId(), request); + return fail(message, null); + } + + String access_token = generateToken(user); + loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request); + return success("登录成功", new LoginResult(access_token, user)); + } + + // 多租户:返回租户列表,不生成 token,前端展示选择列表 + List tenants = superAdmins.stream() + .map(u -> new TenantOption( + u.getTenantId(), + u.getTenantName(), + u.getUserId(), + u.getUsername(), + u.getNickname(), + null, + u.getAvatar() + )) + .collect(Collectors.toList()); + return success("请选择要登录的租户", new LoginResult(tenants)); + } + + @Operation(summary = "选择租户后登录(多租户场景)") + @PostMapping("/loginBySelectTenant") + public ApiResult loginBySelectTenant(@RequestBody LoginParam param, HttpServletRequest request) { + if (param == null) { + return fail("参数不能为空", null); + } + final String phone = param.getPhone(); + if (!CommonUtil.isValidPhoneNumber(phone)) { + return fail("请输入有效的手机号码", null); + } + if (param.getTenantId() == null) { + return fail("请选择租户", null); + } + String code = param.getCode(); + if (StrUtil.isBlank(code)) { + code = param.getSmsCode(); + } + if (StrUtil.isBlank(code)) { + return fail("验证码不能为空", null); + } + + String smsCode = redisUtil.get("code:" + phone); + String devSmsCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS); + if (!StrUtil.equals(code, smsCode) && !StrUtil.equals(code, devSmsCode)) { + String message = "验证码不正确"; + loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, null, request); + return fail(message, null); + } + + // 查询该手机号在指定租户下的超级管理员 + User user = userService.getSuperAdminByPhoneAndTenantId(phone, param.getTenantId()); + if (user == null) { + String message = "该租户下不存在此账号"; + loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, param.getTenantId(), request); + return fail(message, null); + } if (!Boolean.TRUE.equals(user.getIsSuperAdmin())) { String message = "非超级管理员账号不允许登录"; loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, user.getTenantId(), request); return fail(message, null); } - if (!user.getStatus().equals(0)) { - String message = "账号被冻结"; - loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_ERROR, message, user.getTenantId(), request); - return fail(message, null); - } + String access_token = generateToken(user); + loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request); + return success("登录成功", new LoginResult(access_token, user)); + } + + /** + * 生成 JWT token 并存入 Redis + */ + private String generateToken(User user) { Long tokenExpireTime = configProperties.getTokenExpireTime(); final JSONObject register = cacheClient.getSettingInfo("register", user.getTenantId()); if (register != null) { - final String ExpireTime = register.getString("tokenExpireTime"); - if (ExpireTime != null) { - tokenExpireTime = Long.valueOf(ExpireTime); + final String expireTime = register.getString("tokenExpireTime"); + if (expireTime != null) { + tokenExpireTime = Long.valueOf(expireTime); } } - - loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request); String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()), tokenExpireTime, configProperties.getTokenKey()); redisUtil.set("access_token:" + user.getUserId(), access_token, tokenExpireTime, TimeUnit.SECONDS); - return success("登录成功", new LoginResult(access_token, user)); + return access_token; } @Operation(summary = "开发者短信验证码登录") diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java b/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java index cdea5cd..5a7a518 100644 --- a/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java +++ b/src/main/java/com/gxwebsoft/common/system/mapper/UserMapper.java @@ -126,4 +126,25 @@ public interface UserMapper extends BaseMapper { @InterceptorIgnore(tenantLine = "true") User selectAdminByPhoneCrossTenant(@Param("phone") String phone); + /** + * 根据手机号查询所有超级管理员账号(跨租户,忽略租户隔离) + * 用于多租户登录:手机号关联多个租户时返回列表供用户选择 + * + * @param phone 手机号 + * @return List + */ + @InterceptorIgnore(tenantLine = "true") + List selectSuperAdminsByPhone(@Param("phone") String phone); + + /** + * 根据手机号和租户ID查询超级管理员账号(跨租户,忽略租户隔离) + * 用于多租户登录:用户选择租户后,查询该租户下的超级管理员 + * + * @param phone 手机号 + * @param tenantId 租户ID + * @return User + */ + @InterceptorIgnore(tenantLine = "true") + User selectSuperAdminByPhoneAndTenantId(@Param("phone") String phone, @Param("tenantId") Integer tenantId); + } diff --git a/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml b/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml index 5d0a5ad..38a859e 100644 --- a/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml +++ b/src/main/java/com/gxwebsoft/common/system/mapper/xml/UserMapper.xml @@ -461,4 +461,33 @@ LIMIT 1 + + + + + + diff --git a/src/main/java/com/gxwebsoft/common/system/result/LoginResult.java b/src/main/java/com/gxwebsoft/common/system/result/LoginResult.java index 940d8e9..5908aad 100644 --- a/src/main/java/com/gxwebsoft/common/system/result/LoginResult.java +++ b/src/main/java/com/gxwebsoft/common/system/result/LoginResult.java @@ -2,12 +2,11 @@ package com.gxwebsoft.common.system.result; import com.gxwebsoft.common.system.entity.User; import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; +import java.util.List; /** * 登录返回结果 @@ -17,7 +16,6 @@ import java.io.Serializable; */ @Data @NoArgsConstructor -@AllArgsConstructor @Schema(description = "登录返回结果") public class LoginResult implements Serializable { private static final long serialVersionUID = 1L; @@ -28,4 +26,16 @@ public class LoginResult implements Serializable { @Schema(description = "用户信息") private User user; + @Schema(description = "租户列表(多租户场景下返回,此时不返回access_token)") + private List tenants; + + public LoginResult(String access_token, User user) { + this.access_token = access_token; + this.user = user; + } + + public LoginResult(List tenants) { + this.tenants = tenants; + } + } diff --git a/src/main/java/com/gxwebsoft/common/system/result/TenantOption.java b/src/main/java/com/gxwebsoft/common/system/result/TenantOption.java new file mode 100644 index 0000000..f7d11a5 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/result/TenantOption.java @@ -0,0 +1,52 @@ +package com.gxwebsoft.common.system.result; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 租户选项(多租户登录时返回,供前端展示选择列表) + * + * @author WebSoft + */ +@Data +@Schema(description = "租户选项") +public class TenantOption implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "租户ID") + private Integer tenantId; + + @Schema(description = "租户名称") + private String tenantName; + + @Schema(description = "用户ID") + private Integer userId; + + @Schema(description = "用户名") + private String username; + + @Schema(description = "昵称") + private String nickname; + + @Schema(description = "角色名称") + private String roleName; + + @Schema(description = "头像") + private String avatar; + + public TenantOption() { + } + + public TenantOption(Integer tenantId, String tenantName, Integer userId, + String username, String nickname, String roleName, String avatar) { + this.tenantId = tenantId; + this.tenantName = tenantName; + this.userId = userId; + this.username = username; + this.nickname = nickname; + this.roleName = roleName; + this.avatar = avatar; + } +} diff --git a/src/main/java/com/gxwebsoft/common/system/service/UserService.java b/src/main/java/com/gxwebsoft/common/system/service/UserService.java index 7dd41c1..63561f2 100644 --- a/src/main/java/com/gxwebsoft/common/system/service/UserService.java +++ b/src/main/java/com/gxwebsoft/common/system/service/UserService.java @@ -160,6 +160,25 @@ public interface UserService extends IService, UserDetailsService { */ User getByPhoneAndAdmin(String phone); + /** + * 根据手机号查询所有超级管理员账号(跨租户,忽略租户隔离) + * 用于多租户登录:手机号关联多个租户时返回列表供用户选择 + * + * @param phone 手机号 + * @return List + */ + List getSuperAdminsByPhone(String phone); + + /** + * 根据手机号和租户ID查询超级管理员账号(跨租户,忽略租户隔离) + * 用于多租户登录:用户选择租户后,查询该租户下的超级管理员 + * + * @param phone 手机号 + * @param tenantId 租户ID + * @return User + */ + User getSuperAdminByPhoneAndTenantId(String phone, Integer tenantId); + List pageAll(UserParam param); User getByUserId(String userId); diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java index 82cd3df..19bc01a 100644 --- a/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/gxwebsoft/common/system/service/impl/UserServiceImpl.java @@ -460,6 +460,22 @@ public class UserServiceImpl extends ServiceImpl implements Us return baseMapper.selectAdminByPhoneCrossTenant(phone); } + @Override + public List getSuperAdminsByPhone(String phone) { + if (StrUtil.isBlank(phone)) { + return null; + } + return baseMapper.selectSuperAdminsByPhone(phone); + } + + @Override + public User getSuperAdminByPhoneAndTenantId(String phone, Integer tenantId) { + if (StrUtil.isBlank(phone) || tenantId == null) { + return null; + } + return baseMapper.selectSuperAdminByPhoneAndTenantId(phone, tenantId); + } + @Override public List pageAll(UserParam param) { return baseMapper.pageRelAll(param);