feat(auth): 支持多租户超级管理员手机号登录及租户选择功能
- 修改登录接口,支持手机号关联多个超级管理员实现多租户场景 - 多租户登录时返回租户列表供前端选择,不返回 access_token - 新增接口 loginBySelectTenant 选择租户后登录,生成并返回 access_token - 新增 TenantOption 类,封装租户选项信息 - UserMapper 新增跨租户查询超级管理员方法,忽略租户隔离 - UserService 增加对应多租户查询方法并实现 - 调整 SecurityConfig,添加多租户选择登录接口白名单 - LoginResult 新增租户列表字段及构造方法,适配多租户登录返回结果
This commit is contained in:
@@ -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/*",
|
||||
|
||||
@@ -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 = "开发者短信验证码登录")
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user