feat(auth): 支持多租户超级管理员手机号登录及租户选择功能

- 修改登录接口,支持手机号关联多个超级管理员实现多租户场景
- 多租户登录时返回租户列表供前端选择,不返回 access_token
- 新增接口 loginBySelectTenant 选择租户后登录,生成并返回 access_token
- 新增 TenantOption 类,封装租户选项信息
- UserMapper 新增跨租户查询超级管理员方法,忽略租户隔离
- UserService 增加对应多租户查询方法并实现
- 调整 SecurityConfig,添加多租户选择登录接口白名单
- LoginResult 新增租户列表字段及构造方法,适配多租户登录返回结果
This commit is contained in:
2026-07-24 18:49:08 +08:00
parent cb2f856ec7
commit 34c6076068
8 changed files with 240 additions and 16 deletions

View File

@@ -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/*",

View File

@@ -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<User> 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<TenantOption> 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<LoginResult> 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 = "开发者短信验证码登录")

View File

@@ -126,4 +126,25 @@ public interface UserMapper extends BaseMapper<User> {
@InterceptorIgnore(tenantLine = "true")
User selectAdminByPhoneCrossTenant(@Param("phone") String phone);
/**
* 根据手机号查询所有超级管理员账号(跨租户,忽略租户隔离)
* 用于多租户登录:手机号关联多个租户时返回列表供用户选择
*
* @param phone 手机号
* @return List<User>
*/
@InterceptorIgnore(tenantLine = "true")
List<User> 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);
}

View File

@@ -461,4 +461,33 @@
LIMIT 1
</select>
<!-- 根据手机号查询所有超级管理员账号(跨租户,忽略租户隔离,用于多租户登录选择) -->
<select id="selectSuperAdminsByPhone" resultType="com.gxwebsoft.common.system.entity.User">
SELECT u.*,
t.tenant_name
FROM sys_user u
LEFT JOIN sys_tenant t ON u.tenant_id = t.tenant_id
WHERE u.deleted = 0
AND u.status = 0
AND u.phone = #{phone}
AND u.is_super_admin = 1
ORDER BY u.update_time DESC,
u.create_time DESC,
u.user_id DESC
</select>
<!-- 根据手机号和租户ID查询超级管理员账号跨租户忽略租户隔离用于选择租户后登录 -->
<select id="selectSuperAdminByPhoneAndTenantId" resultType="com.gxwebsoft.common.system.entity.User">
SELECT u.*,
t.tenant_name
FROM sys_user u
LEFT JOIN sys_tenant t ON u.tenant_id = t.tenant_id
WHERE u.deleted = 0
AND u.status = 0
AND u.phone = #{phone}
AND u.is_super_admin = 1
AND u.tenant_id = #{tenantId}
LIMIT 1
</select>
</mapper>

View File

@@ -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<TenantOption> tenants;
public LoginResult(String access_token, User user) {
this.access_token = access_token;
this.user = user;
}
public LoginResult(List<TenantOption> tenants) {
this.tenants = tenants;
}
}

View File

@@ -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;
}
}

View File

@@ -160,6 +160,25 @@ public interface UserService extends IService<User>, UserDetailsService {
*/
User getByPhoneAndAdmin(String phone);
/**
* 根据手机号查询所有超级管理员账号(跨租户,忽略租户隔离)
* 用于多租户登录:手机号关联多个租户时返回列表供用户选择
*
* @param phone 手机号
* @return List<User>
*/
List<User> getSuperAdminsByPhone(String phone);
/**
* 根据手机号和租户ID查询超级管理员账号跨租户忽略租户隔离
* 用于多租户登录:用户选择租户后,查询该租户下的超级管理员
*
* @param phone 手机号
* @param tenantId 租户ID
* @return User
*/
User getSuperAdminByPhoneAndTenantId(String phone, Integer tenantId);
List<User> pageAll(UserParam param);
User getByUserId(String userId);

View File

@@ -460,6 +460,22 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
return baseMapper.selectAdminByPhoneCrossTenant(phone);
}
@Override
public List<User> 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<User> pageAll(UserParam param) {
return baseMapper.pageRelAll(param);