add 框架整体 整合适配多租户功能

This commit is contained in:
疯狂的狮子li 2023-02-08 15:59:00 +08:00
parent 39d74a7780
commit 5b08bba16c
32 changed files with 366 additions and 46 deletions

View File

@ -41,16 +41,18 @@ public class SysLoginController {
/** /**
* 登录方法 * 登录方法
* *
* @param loginBody 登录信息 * @param body 登录信息
* @return 结果 * @return 结果
*/ */
@SaIgnore @SaIgnore
@PostMapping("/login") @PostMapping("/login")
public R<LoginVo> login(@Validated @RequestBody LoginBody loginBody) { public R<LoginVo> login(@Validated @RequestBody LoginBody body) {
LoginVo loginVo = new LoginVo(); LoginVo loginVo = new LoginVo();
// 生成令牌 // 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), String token = loginService.login(
loginBody.getUuid()); body.getTenantId(),
body.getUsername(), body.getPassword(),
body.getCode(), body.getUuid());
loginVo.setToken(token); loginVo.setToken(token);
return R.ok(loginVo); return R.ok(loginVo);
} }
@ -58,15 +60,15 @@ public class SysLoginController {
/** /**
* 短信登录(示例) * 短信登录(示例)
* *
* @param smsLoginBody 登录信息 * @param body 登录信息
* @return 结果 * @return 结果
*/ */
@SaIgnore @SaIgnore
@PostMapping("/smsLogin") @PostMapping("/smsLogin")
public R<LoginVo> smsLogin(@Validated @RequestBody SmsLoginBody smsLoginBody) { public R<LoginVo> smsLogin(@Validated @RequestBody SmsLoginBody body) {
LoginVo loginVo = new LoginVo(); LoginVo loginVo = new LoginVo();
// 生成令牌 // 生成令牌
String token = loginService.smsLogin(smsLoginBody.getPhonenumber(), smsLoginBody.getSmsCode()); String token = loginService.smsLogin(body.getTenantId(), body.getPhonenumber(), body.getSmsCode());
loginVo.setToken(token); loginVo.setToken(token);
return R.ok(loginVo); return R.ok(loginVo);
} }

View File

@ -13,7 +13,9 @@ import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.domain.model.XcxLoginUser; import com.ruoyi.common.core.domain.model.XcxLoginUser;
import com.ruoyi.common.core.enums.DeviceType; import com.ruoyi.common.core.enums.DeviceType;
import com.ruoyi.common.core.enums.LoginType; import com.ruoyi.common.core.enums.LoginType;
import com.ruoyi.common.core.enums.TenantStatus;
import com.ruoyi.common.core.enums.UserStatus; import com.ruoyi.common.core.enums.UserStatus;
import com.ruoyi.common.core.exception.TenantException;
import com.ruoyi.common.core.exception.user.CaptchaException; import com.ruoyi.common.core.exception.user.CaptchaException;
import com.ruoyi.common.core.exception.user.CaptchaExpireException; import com.ruoyi.common.core.exception.user.CaptchaExpireException;
import com.ruoyi.common.core.exception.user.UserException; import com.ruoyi.common.core.exception.user.UserException;
@ -22,10 +24,12 @@ import com.ruoyi.common.log.event.LogininforEvent;
import com.ruoyi.common.redis.utils.RedisUtils; import com.ruoyi.common.redis.utils.RedisUtils;
import com.ruoyi.common.satoken.utils.LoginHelper; import com.ruoyi.common.satoken.utils.LoginHelper;
import com.ruoyi.system.domain.SysUser; import com.ruoyi.system.domain.SysUser;
import com.ruoyi.system.domain.vo.SysTenantVo;
import com.ruoyi.system.domain.vo.SysUserVo; import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.system.mapper.SysUserMapper; import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.service.ISysConfigService; import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysPermissionService; import com.ruoyi.system.service.ISysPermissionService;
import com.ruoyi.system.service.ISysTenantService;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -33,6 +37,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Duration; import java.time.Duration;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -49,6 +54,7 @@ public class SysLoginService {
private final SysUserMapper userMapper; private final SysUserMapper userMapper;
private final ISysConfigService configService; private final ISysConfigService configService;
private final ISysPermissionService permissionService; private final ISysPermissionService permissionService;
private final ISysTenantService tenantService;
@Value("${user.password.maxRetryCount}") @Value("${user.password.maxRetryCount}")
private Integer maxRetryCount; private Integer maxRetryCount;
@ -56,6 +62,9 @@ public class SysLoginService {
@Value("${user.password.lockTime}") @Value("${user.password.lockTime}")
private Integer lockTime; private Integer lockTime;
@Value("${tenant.enable}")
private Boolean tenantEnable;
/** /**
* 登录验证 * 登录验证
* *
@ -65,36 +74,41 @@ public class SysLoginService {
* @param uuid 唯一标识 * @param uuid 唯一标识
* @return 结果 * @return 结果
*/ */
public String login(String username, String password, String code, String uuid) { public String login(String tenantId, String username, String password, String code, String uuid) {
HttpServletRequest request = ServletUtils.getRequest(); HttpServletRequest request = ServletUtils.getRequest();
boolean captchaEnabled = configService.selectCaptchaEnabled(); boolean captchaEnabled = configService.selectCaptchaEnabled();
// 验证码开关 // 验证码开关
if (captchaEnabled) { if (captchaEnabled) {
validateCaptcha(username, code, uuid, request); validateCaptcha(tenantId, username, code, uuid, request);
} }
SysUserVo user = loadUserByUsername(username); // 校验租户
checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword())); checkTenant(tenantId);
SysUserVo user = loadUserByUsername(tenantId, username);
checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
// 此处可根据登录用户的数据不同 自行创建 loginUser // 此处可根据登录用户的数据不同 自行创建 loginUser
LoginUser loginUser = buildLoginUser(user); LoginUser loginUser = buildLoginUser(user);
// 生成token // 生成token
LoginHelper.loginByDevice(loginUser, DeviceType.PC); LoginHelper.loginByDevice(loginUser, DeviceType.PC);
recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")); recordLogininfor(loginUser.getTenantId(), username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
recordLoginInfo(user.getUserId()); recordLoginInfo(user.getUserId());
return StpUtil.getTokenValue(); return StpUtil.getTokenValue();
} }
public String smsLogin(String phonenumber, String smsCode) { public String smsLogin(String tenantId, String phonenumber, String smsCode) {
// 校验租户
checkTenant(tenantId);
// 通过手机号查找用户 // 通过手机号查找用户
SysUserVo user = loadUserByPhonenumber(phonenumber); SysUserVo user = loadUserByPhonenumber(tenantId, phonenumber);
checkLogin(LoginType.SMS, user.getUserName(), () -> !validateSmsCode(phonenumber, smsCode)); checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
// 此处可根据登录用户的数据不同 自行创建 loginUser // 此处可根据登录用户的数据不同 自行创建 loginUser
LoginUser loginUser = buildLoginUser(user); LoginUser loginUser = buildLoginUser(user);
// 生成token // 生成token
LoginHelper.loginByDevice(loginUser, DeviceType.APP); LoginHelper.loginByDevice(loginUser, DeviceType.APP);
recordLogininfor(user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")); recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
recordLoginInfo(user.getUserId()); recordLoginInfo(user.getUserId());
return StpUtil.getTokenValue(); return StpUtil.getTokenValue();
} }
@ -106,9 +120,12 @@ public class SysLoginService {
// 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key openid // 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key openid
String openid = ""; String openid = "";
SysUserVo user = loadUserByOpenid(openid); SysUserVo user = loadUserByOpenid(openid);
// 校验租户
checkTenant(user.getTenantId());
// 此处可根据登录用户的数据不同 自行创建 loginUser // 此处可根据登录用户的数据不同 自行创建 loginUser
XcxLoginUser loginUser = new XcxLoginUser(); XcxLoginUser loginUser = new XcxLoginUser();
loginUser.setTenantId(user.getTenantId());
loginUser.setUserId(user.getUserId()); loginUser.setUserId(user.getUserId());
loginUser.setUsername(user.getUserName()); loginUser.setUsername(user.getUserName());
loginUser.setUserType(user.getUserType()); loginUser.setUserType(user.getUserType());
@ -116,7 +133,7 @@ public class SysLoginService {
// 生成token // 生成token
LoginHelper.loginByDevice(loginUser, DeviceType.XCX); LoginHelper.loginByDevice(loginUser, DeviceType.XCX);
recordLogininfor(user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")); recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
recordLoginInfo(user.getUserId()); recordLoginInfo(user.getUserId());
return StpUtil.getTokenValue(); return StpUtil.getTokenValue();
} }
@ -128,7 +145,7 @@ public class SysLoginService {
try { try {
LoginUser loginUser = LoginHelper.getLoginUser(); LoginUser loginUser = LoginHelper.getLoginUser();
StpUtil.logout(); StpUtil.logout();
recordLogininfor(loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success")); recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
} catch (NotLoginException ignored) { } catch (NotLoginException ignored) {
} }
} }
@ -136,13 +153,15 @@ public class SysLoginService {
/** /**
* 记录登录信息 * 记录登录信息
* *
* @param tenantId 租户ID
* @param username 用户名 * @param username 用户名
* @param status 状态 * @param status 状态
* @param message 消息内容 * @param message 消息内容
* @return * @return
*/ */
private void recordLogininfor(String username, String status, String message) { private void recordLogininfor(String tenantId, String username, String status, String message) {
LogininforEvent logininforEvent = new LogininforEvent(); LogininforEvent logininforEvent = new LogininforEvent();
logininforEvent.setTenantId(tenantId);
logininforEvent.setUsername(username); logininforEvent.setUsername(username);
logininforEvent.setStatus(status); logininforEvent.setStatus(status);
logininforEvent.setMessage(message); logininforEvent.setMessage(message);
@ -153,10 +172,10 @@ public class SysLoginService {
/** /**
* 校验短信验证码 * 校验短信验证码
*/ */
private boolean validateSmsCode(String phonenumber, String smsCode) { private boolean validateSmsCode(String tenantId, String phonenumber, String smsCode) {
String code = RedisUtils.getCacheObject(CacheConstants.CAPTCHA_CODE_KEY + phonenumber); String code = RedisUtils.getCacheObject(CacheConstants.CAPTCHA_CODE_KEY + phonenumber);
if (StringUtils.isBlank(code)) { if (StringUtils.isBlank(code)) {
recordLogininfor(phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")); recordLogininfor(tenantId, phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException(); throw new CaptchaExpireException();
} }
return code.equals(smsCode); return code.equals(smsCode);
@ -169,23 +188,24 @@ public class SysLoginService {
* @param code 验证码 * @param code 验证码
* @param uuid 唯一标识 * @param uuid 唯一标识
*/ */
public void validateCaptcha(String username, String code, String uuid, HttpServletRequest request) { public void validateCaptcha(String tenantId, String username, String code, String uuid, HttpServletRequest request) {
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, ""); String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, "");
String captcha = RedisUtils.getCacheObject(verifyKey); String captcha = RedisUtils.getCacheObject(verifyKey);
RedisUtils.deleteObject(verifyKey); RedisUtils.deleteObject(verifyKey);
if (captcha == null) { if (captcha == null) {
recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")); recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException(); throw new CaptchaExpireException();
} }
if (!code.equalsIgnoreCase(captcha)) { if (!code.equalsIgnoreCase(captcha)) {
recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")); recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
throw new CaptchaException(); throw new CaptchaException();
} }
} }
private SysUserVo loadUserByUsername(String username) { private SysUserVo loadUserByUsername(String tenantId, String username) {
SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>() SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
.select(SysUser::getUserName, SysUser::getStatus) .select(SysUser::getUserName, SysUser::getStatus)
.eq(tenantEnable, SysUser::getTenantId, tenantId)
.eq(SysUser::getUserName, username)); .eq(SysUser::getUserName, username));
if (ObjectUtil.isNull(user)) { if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", username); log.info("登录用户:{} 不存在.", username);
@ -197,9 +217,10 @@ public class SysLoginService {
return userMapper.selectUserByUserName(username); return userMapper.selectUserByUserName(username);
} }
private SysUserVo loadUserByPhonenumber(String phonenumber) { private SysUserVo loadUserByPhonenumber(String tenantId, String phonenumber) {
SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>() SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
.select(SysUser::getPhonenumber, SysUser::getStatus) .select(SysUser::getPhonenumber, SysUser::getStatus)
.eq(tenantEnable, SysUser::getTenantId, tenantId)
.eq(SysUser::getPhonenumber, phonenumber)); .eq(SysUser::getPhonenumber, phonenumber));
if (ObjectUtil.isNull(user)) { if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", phonenumber); log.info("登录用户:{} 不存在.", phonenumber);
@ -230,6 +251,7 @@ public class SysLoginService {
*/ */
private LoginUser buildLoginUser(SysUserVo user) { private LoginUser buildLoginUser(SysUserVo user) {
LoginUser loginUser = new LoginUser(); LoginUser loginUser = new LoginUser();
loginUser.setTenantId(user.getTenantId());
loginUser.setUserId(user.getUserId()); loginUser.setUserId(user.getUserId());
loginUser.setDeptId(user.getDeptId()); loginUser.setDeptId(user.getDeptId());
loginUser.setUsername(user.getUserName()); loginUser.setUsername(user.getUserName());
@ -259,7 +281,7 @@ public class SysLoginService {
/** /**
* 登录校验 * 登录校验
*/ */
private void checkLogin(LoginType loginType, String username, Supplier<Boolean> supplier) { private void checkLogin(LoginType loginType, String tenantId, String username, Supplier<Boolean> supplier) {
String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username; String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
String loginFail = Constants.LOGIN_FAIL; String loginFail = Constants.LOGIN_FAIL;
@ -267,7 +289,7 @@ public class SysLoginService {
Integer errorNumber = RedisUtils.getCacheObject(errorKey); Integer errorNumber = RedisUtils.getCacheObject(errorKey);
// 锁定时间内登录 则踢出 // 锁定时间内登录 则踢出
if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) { if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime)); recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime); throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
} }
@ -277,12 +299,12 @@ public class SysLoginService {
// 达到规定错误次数 则锁定登录 // 达到规定错误次数 则锁定登录
if (errorNumber.equals(maxRetryCount)) { if (errorNumber.equals(maxRetryCount)) {
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime)); RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime)); recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime); throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
} else { } else {
// 未达到规定错误次数 则递增 // 未达到规定错误次数 则递增
RedisUtils.setCacheObject(errorKey, errorNumber); RedisUtils.setCacheObject(errorKey, errorNumber);
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber)); recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
throw new UserException(loginType.getRetryLimitCount(), errorNumber); throw new UserException(loginType.getRetryLimitCount(), errorNumber);
} }
} }
@ -290,4 +312,22 @@ public class SysLoginService {
// 登录成功 清空错误次数 // 登录成功 清空错误次数
RedisUtils.deleteObject(errorKey); RedisUtils.deleteObject(errorKey);
} }
private void checkTenant(String tenantId) {
if (tenantEnable) {
return;
}
SysTenantVo tenant = tenantService.queryByTenantId(tenantId);
if (ObjectUtil.isNull(tenant)) {
log.info("登录租户:{} 不存在.", tenantId);
throw new TenantException("tenant.not.exists");
} else if (TenantStatus.DISABLE.getCode().equals(tenant.getStatus())) {
log.info("登录租户:{} 已被停用.", tenantId);
throw new TenantException("tenant.blocked");
} else if (tenant.getExpireTime() != null && new Date().after(tenant.getExpireTime())) {
log.info("登录租户:{} 已超过有效期.", tenantId);
throw new TenantException("tenant.expired");
}
}
} }

View File

@ -39,6 +39,7 @@ public class SysRegisterService {
*/ */
public void register(RegisterBody registerBody) { public void register(RegisterBody registerBody) {
HttpServletRequest request = ServletUtils.getRequest(); HttpServletRequest request = ServletUtils.getRequest();
String tenantId = registerBody.getTenantId();
String username = registerBody.getUsername(); String username = registerBody.getUsername();
String password = registerBody.getPassword(); String password = registerBody.getPassword();
// 校验用户类型是否存在 // 校验用户类型是否存在
@ -47,9 +48,10 @@ public class SysRegisterService {
boolean captchaEnabled = configService.selectCaptchaEnabled(); boolean captchaEnabled = configService.selectCaptchaEnabled();
// 验证码开关 // 验证码开关
if (captchaEnabled) { if (captchaEnabled) {
validateCaptcha(username, registerBody.getCode(), registerBody.getUuid(), request); validateCaptcha(tenantId, username, registerBody.getCode(), registerBody.getUuid(), request);
} }
SysUserBo sysUser = new SysUserBo(); SysUserBo sysUser = new SysUserBo();
sysUser.setTenantId(tenantId);
sysUser.setUserName(username); sysUser.setUserName(username);
sysUser.setNickName(username); sysUser.setNickName(username);
sysUser.setPassword(BCrypt.hashpw(password)); sysUser.setPassword(BCrypt.hashpw(password));
@ -62,7 +64,7 @@ public class SysRegisterService {
if (!regFlag) { if (!regFlag) {
throw new UserException("user.register.error"); throw new UserException("user.register.error");
} }
recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.register.success")); recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.register.success"));
} }
/** /**
@ -73,16 +75,16 @@ public class SysRegisterService {
* @param uuid 唯一标识 * @param uuid 唯一标识
* @return 结果 * @return 结果
*/ */
public void validateCaptcha(String username, String code, String uuid, HttpServletRequest request) { public void validateCaptcha(String tenantId, String username, String code, String uuid, HttpServletRequest request) {
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, ""); String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, "");
String captcha = RedisUtils.getCacheObject(verifyKey); String captcha = RedisUtils.getCacheObject(verifyKey);
RedisUtils.deleteObject(verifyKey); RedisUtils.deleteObject(verifyKey);
if (captcha == null) { if (captcha == null) {
recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.jcaptcha.expire")); recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException(); throw new CaptchaExpireException();
} }
if (!code.equalsIgnoreCase(captcha)) { if (!code.equalsIgnoreCase(captcha)) {
recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.jcaptcha.error")); recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.jcaptcha.error"));
throw new CaptchaException(); throw new CaptchaException();
} }
} }
@ -90,13 +92,15 @@ public class SysRegisterService {
/** /**
* 记录登录信息 * 记录登录信息
* *
* @param tenantId 租户ID
* @param username 用户名 * @param username 用户名
* @param status 状态 * @param status 状态
* @param message 消息内容 * @param message 消息内容
* @return * @return
*/ */
private void recordLogininfor(String username, String status, String message) { private void recordLogininfor(String tenantId, String username, String status, String message) {
LogininforEvent logininforEvent = new LogininforEvent(); LogininforEvent logininforEvent = new LogininforEvent();
logininforEvent.setTenantId(tenantId);
logininforEvent.setUsername(username); logininforEvent.setUsername(username);
logininforEvent.setStatus(status); logininforEvent.setStatus(status);
logininforEvent.setMessage(message); logininforEvent.setMessage(message);

View File

@ -136,6 +136,21 @@ security:
- /actuator - /actuator
- /actuator/** - /actuator/**
# 多租户配置
tenant:
# 是否开启
enable: true
# 排除表
excludes:
- sys_menu
- sys_config
- sys_dict_type
- sys_dict_data
- sys_role_dept
- sys_role_menu
- sys_user_post
- sys_user_role
# MyBatisPlus配置 # MyBatisPlus配置
# https://baomidou.com/config/ # https://baomidou.com/config/
mybatis-plus: mybatis-plus:

View File

@ -43,3 +43,8 @@ sms.code.not.blank=短信验证码不能为空
sms.code.retry.limit.count=短信验证码输入错误{0}次 sms.code.retry.limit.count=短信验证码输入错误{0}次
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟 sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟
xcx.code.not.blank=小程序code不能为空 xcx.code.not.blank=小程序code不能为空
##租户
tenant.number.not.blank=租户编号不能为空
tenant.not.exists=对不起, 您的租户不存在,请联系管理员
tenant.blocked=对不起,您的租户已禁用,请联系管理员
tenant.expired=对不起,您的租户已过期,请联系管理员

View File

@ -43,3 +43,8 @@ sms.code.not.blank=Sms code cannot be blank
sms.code.retry.limit.count=Sms code input error {0} times sms.code.retry.limit.count=Sms code input error {0} times
sms.code.retry.limit.exceed=Sms code input error {0} times, account locked for {1} minutes sms.code.retry.limit.exceed=Sms code input error {0} times, account locked for {1} minutes
xcx.code.not.blank=Mini program code cannot be blank xcx.code.not.blank=Mini program code cannot be blank
##租户
tenant.number.not.blank=Tenant number cannot be blank
tenant.not.exists=Sorry, your tenant does not exist. Please contact the administrator
tenant.blocked=Sorry, your tenant is disabled. Please contact the administrator
tenant.expired=Sorry, your tenant has expired. Please contact the administrator.

View File

@ -43,3 +43,8 @@ sms.code.not.blank=短信验证码不能为空
sms.code.retry.limit.count=短信验证码输入错误{0}次 sms.code.retry.limit.count=短信验证码输入错误{0}次
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟 sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟
xcx.code.not.blank=小程序code不能为空 xcx.code.not.blank=小程序code不能为空
##租户
tenant.number.not.blank=租户编号不能为空
tenant.not.exists=对不起, 您的租户不存在,请联系管理员
tenant.blocked=对不起,您的租户已禁用,请联系管理员
tenant.expired=对不起,您的租户已过期,请联系管理员

View File

@ -15,6 +15,12 @@ import jakarta.validation.constraints.NotBlank;
@Data @Data
public class LoginBody { public class LoginBody {
/**
* 租户ID
*/
@NotBlank(message = "{tenant.number.not.blank}")
private String tenantId;
/** /**
* 用户名 * 用户名
*/ */

View File

@ -22,6 +22,11 @@ public class LoginUser implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/** /**
* 用户ID * 用户ID
*/ */

View File

@ -13,6 +13,12 @@ import jakarta.validation.constraints.NotBlank;
@Data @Data
public class SmsLoginBody { public class SmsLoginBody {
/**
* 租户ID
*/
@NotBlank(message = "{tenant.number.not.blank}")
private String tenantId;
/** /**
* 用户名 * 用户名
*/ */

View File

@ -0,0 +1,30 @@
package com.ruoyi.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 用户状态
*
* @author LionLi
*/
@Getter
@AllArgsConstructor
public enum TenantStatus {
/**
* 正常
*/
OK("0", "正常"),
/**
* 停用
*/
DISABLE("1", "停用"),
/**
* 删除
*/
DELETED("2", "删除");
private final String code;
private final String info;
}

View File

@ -0,0 +1,20 @@
package com.ruoyi.common.core.exception;
import com.ruoyi.common.core.exception.base.BaseException;
import java.io.Serial;
/**
* 用户信息异常类
*
* @author ruoyi
*/
public class TenantException extends BaseException {
@Serial
private static final long serialVersionUID = 1L;
public TenantException(String code, Object... args) {
super("tenant", code, args, null);
}
}

View File

@ -67,6 +67,7 @@ public class LogAspect {
// *========数据库日志=========*// // *========数据库日志=========*//
OperLogEvent operLog = new OperLogEvent(); OperLogEvent operLog = new OperLogEvent();
operLog.setTenantId(LoginHelper.getTenantId());
operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址 // 请求的地址
String ip = ServletUtils.getClientIP(); String ip = ServletUtils.getClientIP();

View File

@ -19,6 +19,11 @@ public class LogininforEvent implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/** /**
* 用户账号 * 用户账号
*/ */

View File

@ -23,6 +23,11 @@ public class OperLogEvent implements Serializable {
*/ */
private Long operId; private Long operId;
/**
* 租户ID
*/
private String tenantId;
/** /**
* 操作模块 * 操作模块
*/ */

View File

@ -7,10 +7,14 @@ import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.ruoyi.common.mybatis.handler.InjectionMetaObjectHandler; import com.ruoyi.common.mybatis.handler.InjectionMetaObjectHandler;
import com.ruoyi.common.mybatis.handler.PlusTenantLineHandler;
import com.ruoyi.common.mybatis.interceptor.PlusDataPermissionInterceptor; import com.ruoyi.common.mybatis.interceptor.PlusDataPermissionInterceptor;
import com.ruoyi.common.mybatis.properties.TenantProperties;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.EnableTransactionManagement;
@ -19,14 +23,19 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
* *
* @author Lion Li * @author Lion Li
*/ */
@EnableConfigurationProperties(TenantProperties.class)
@EnableTransactionManagement(proxyTargetClass = true) @EnableTransactionManagement(proxyTargetClass = true)
@AutoConfiguration @AutoConfiguration
@MapperScan("${mybatis-plus.mapperPackage}") @MapperScan("${mybatis-plus.mapperPackage}")
public class MybatisPlusConfig { public class MybatisPlusConfig {
@Bean @Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { public MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties tenantProperties) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
if (tenantProperties.getEnable()) {
// 多租户插件
interceptor.addInnerInterceptor(tenantLineInnerInterceptor(tenantProperties));
}
// 数据权限处理 // 数据权限处理
interceptor.addInnerInterceptor(dataPermissionInterceptor()); interceptor.addInnerInterceptor(dataPermissionInterceptor());
// 分页插件 // 分页插件
@ -36,6 +45,13 @@ public class MybatisPlusConfig {
return interceptor; return interceptor;
} }
/**
* 多租户插件
*/
public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties tenantProperties) {
return new TenantLineInnerInterceptor(new PlusTenantLineHandler(tenantProperties));
}
/** /**
* 数据权限拦截器 * 数据权限拦截器
*/ */

View File

@ -15,6 +15,6 @@ public class TenantEntity extends BaseEntity {
/** /**
* 租户编号 * 租户编号
*/ */
private String tenantId = "000000"; private String tenantId;
} }

View File

@ -0,0 +1,54 @@
package com.ruoyi.common.mybatis.handler;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.mybatis.properties.TenantProperties;
import com.ruoyi.common.satoken.utils.LoginHelper;
import lombok.AllArgsConstructor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.NullValue;
import java.util.List;
/**
* 自定义租户处理器
*
* @author Lion Li
*/
@AllArgsConstructor
public class PlusTenantLineHandler implements TenantLineHandler {
private final TenantProperties tenantProperties;
@Override
public Expression getTenantId() {
String tenantId = LoginHelper.getTenantId();
if (StringUtils.isNotBlank(tenantId)) {
//多租户模式租户id从当前用户获取
return new LongValue(tenantId);
}
return new NullValue();
}
@Override
public boolean ignoreTable(String tableName) {
String tenantId = LoginHelper.getTenantId();
// 判断是否有租户
if (StringUtils.isNotBlank(tenantId)) {
// 判断是否平台超级管理员如果是平台超级管理员则拥有所有数据权限
if (!LoginHelper.isAdmin()) {
// 不需要过滤租户的表
List<String> excludes = tenantProperties.getExcludes();
// 非业务表
excludes.addAll(List.of(
"gen_table",
"gen_table_column"
));
return excludes.contains(tableName);
}
}
return true;
}
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.common.mybatis.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
/**
* 租户 配置属性
*
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "tenant")
public class TenantProperties {
/**
* 是否启用
*/
private Boolean enable;
/**
* 排除表
*/
private List<String> excludes;
}

View File

@ -96,6 +96,19 @@ public class LoginHelper {
return loginUser.getUserId(); return loginUser.getUserId();
} }
/**
* 获取租户ID
*/
public static String getTenantId() {
LoginUser loginUser;
try {
loginUser = getLoginUser();
} catch (Exception e) {
return null;
}
return loginUser.getTenantId();
}
/** /**
* 获取部门ID * 获取部门ID
*/ */

View File

@ -23,7 +23,7 @@ public class SysLogininforBo {
/** /**
* 租户编号 * 租户编号
*/ */
private String tenantId = "000000"; private String tenantId;
/** /**
* 用户账号 * 用户账号

View File

@ -25,7 +25,7 @@ public class SysOperLogBo {
/** /**
* 租户编号 * 租户编号
*/ */
private String tenantId = "000000"; private String tenantId;
/** /**
* 模块标题 * 模块标题

View File

@ -27,6 +27,11 @@ public class SysUserVo implements Serializable {
*/ */
private Long userId; private Long userId;
/**
* 租户ID
*/
private String tenantId;
/** /**
* 部门ID * 部门ID
*/ */

View File

@ -1,5 +1,7 @@
package com.ruoyi.system.runner; package com.ruoyi.system.runner;
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.ruoyi.common.core.config.RuoYiConfig; import com.ruoyi.common.core.config.RuoYiConfig;
import com.ruoyi.system.service.ISysConfigService; import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysDictTypeService; import com.ruoyi.system.service.ISysDictTypeService;
@ -27,15 +29,18 @@ public class SystemApplicationRunner implements ApplicationRunner {
@Override @Override
public void run(ApplicationArguments args) throws Exception { public void run(ApplicationArguments args) throws Exception {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
ossConfigService.init(); ossConfigService.init();
log.info("初始化OSS配置成功"); log.info("初始化OSS配置成功");
if (ruoyiConfig.isCacheLazy()) { if (ruoyiConfig.isCacheLazy()) {
InterceptorIgnoreHelper.clearIgnoreStrategy();
return; return;
} }
configService.loadingConfigCache(); configService.loadingConfigCache();
log.info("加载参数缓存数据成功"); log.info("加载参数缓存数据成功");
dictTypeService.loadingDictCache(); dictTypeService.loadingDictCache();
log.info("加载字典缓存数据成功"); log.info("加载字典缓存数据成功");
InterceptorIgnoreHelper.clearIgnoreStrategy();
} }
} }

View File

@ -20,6 +20,11 @@ public interface ISysTenantService {
*/ */
SysTenantVo queryById(Long id); SysTenantVo queryById(Long id);
/**
* 基于租户ID查询租户
*/
SysTenantVo queryByTenantId(String tenantId);
/** /**
* 查询租户列表 * 查询租户列表
*/ */

View File

@ -68,6 +68,7 @@ public class SysLogininforServiceImpl implements ISysLogininforService {
String browser = userAgent.getBrowser().getName(); String browser = userAgent.getBrowser().getName();
// 封装对象 // 封装对象
SysLogininforBo logininfor = new SysLogininforBo(); SysLogininforBo logininfor = new SysLogininforBo();
logininfor.setTenantId(logininforEvent.getTenantId());
logininfor.setUserName(logininforEvent.getUsername()); logininfor.setUserName(logininforEvent.getUsername());
logininfor.setIpaddr(ip); logininfor.setIpaddr(ip);
logininfor.setLoginLocation(address); logininfor.setLoginLocation(address);

View File

@ -53,6 +53,11 @@ public class SysTenantServiceImpl implements ISysTenantService {
return baseMapper.selectVoById(id); return baseMapper.selectVoById(id);
} }
@Override
public SysTenantVo queryByTenantId(String tenantId) {
return baseMapper.selectVoOne(new LambdaQueryWrapper<SysTenant>().eq(SysTenant::getTenantId, tenantId));
}
/** /**
* 查询租户列表 * 查询租户列表
*/ */

View File

@ -22,6 +22,7 @@
<sql id="selectUserVo"> <sql id="selectUserVo">
select u.user_id, select u.user_id,
u.tenant_id,
u.dept_id, u.dept_id,
u.user_name, u.user_name,
u.nick_name, u.nick_name,

View File

@ -1,8 +1,9 @@
import request from '@/utils/request' import request from '@/utils/request'
// 登录方法 // 登录方法
export function login(username, password, code, uuid) { export function login(tenantId, username, password, code, uuid) {
const data = { const data = {
tenantId,
username, username,
password, password,
code, code,

View File

@ -31,12 +31,13 @@ const user = {
actions: { actions: {
// 登录 // 登录
Login({ commit }, userInfo) { Login({ commit }, userInfo) {
const tenantId = userInfo.tenantId.trim()
const username = userInfo.username.trim() const username = userInfo.username.trim()
const password = userInfo.password const password = userInfo.password
const code = userInfo.code const code = userInfo.code
const uuid = userInfo.uuid const uuid = userInfo.uuid
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => { login(tenantId, username, password, code, uuid).then(res => {
setToken(res.data.token) setToken(res.data.token)
commit('SET_TOKEN', res.data.token) commit('SET_TOKEN', res.data.token)
resolve() resolve()

View File

@ -2,6 +2,16 @@
<div class="login"> <div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form"> <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">RuoYi-Vue-Plus后台管理系统</h3> <h3 class="title">RuoYi-Vue-Plus后台管理系统</h3>
<el-form-item prop="tenantId">
<el-input
v-model="loginForm.tenantId"
type="text"
auto-complete="off"
placeholder="租户编号"
>
<svg-icon slot="prefix" icon-class="input" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input
v-model="loginForm.username" v-model="loginForm.username"
@ -72,6 +82,7 @@ export default {
return { return {
codeUrl: "", codeUrl: "",
loginForm: { loginForm: {
tenantId: "000000",
username: "admin", username: "admin",
password: "admin123", password: "admin123",
rememberMe: false, rememberMe: false,
@ -79,6 +90,9 @@ export default {
uuid: "" uuid: ""
}, },
loginRules: { loginRules: {
tenantId: [
{ required: true, trigger: "blur", message: "请输入您的租户编号" }
],
username: [ username: [
{ required: true, trigger: "blur", message: "请输入您的账号" } { required: true, trigger: "blur", message: "请输入您的账号" }
], ],
@ -118,10 +132,12 @@ export default {
}); });
}, },
getCookie() { getCookie() {
const tenantId = Cookies.get("tenantId");
const username = Cookies.get("username"); const username = Cookies.get("username");
const password = Cookies.get("password"); const password = Cookies.get("password");
const rememberMe = Cookies.get('rememberMe') const rememberMe = Cookies.get('rememberMe')
this.loginForm = { this.loginForm = {
tenantId: tenantId === undefined ? this.loginForm.tenantId : tenantId,
username: username === undefined ? this.loginForm.username : username, username: username === undefined ? this.loginForm.username : username,
password: password === undefined ? this.loginForm.password : decrypt(password), password: password === undefined ? this.loginForm.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe) rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
@ -132,10 +148,12 @@ export default {
if (valid) { if (valid) {
this.loading = true; this.loading = true;
if (this.loginForm.rememberMe) { if (this.loginForm.rememberMe) {
Cookies.set("tenantId", this.loginForm.tenantId, { expires: 30 });
Cookies.set("username", this.loginForm.username, { expires: 30 }); Cookies.set("username", this.loginForm.username, { expires: 30 });
Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 }); Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 }); Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
} else { } else {
Cookies.remove("tenantId");
Cookies.remove("username"); Cookies.remove("username");
Cookies.remove("password"); Cookies.remove("password");
Cookies.remove('rememberMe'); Cookies.remove('rememberMe');

View File

@ -2,6 +2,16 @@
<div class="register"> <div class="register">
<el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form"> <el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form">
<h3 class="title">RuoYi-Vue-Plus后台管理系统</h3> <h3 class="title">RuoYi-Vue-Plus后台管理系统</h3>
<el-form-item prop="tenantId">
<el-input
v-model="loginForm.tenantId"
type="text"
auto-complete="off"
placeholder="租户编号"
>
<svg-icon slot="prefix" icon-class="input" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="username"> <el-form-item prop="username">
<el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号"> <el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" /> <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
@ -82,6 +92,7 @@ export default {
return { return {
codeUrl: "", codeUrl: "",
registerForm: { registerForm: {
tenantId: "",
username: "", username: "",
password: "", password: "",
confirmPassword: "", confirmPassword: "",
@ -90,6 +101,9 @@ export default {
userType: "sys_user" userType: "sys_user"
}, },
registerRules: { registerRules: {
tenantId: [
{ required: true, trigger: "blur", message: "请输入您的租户编号" }
],
username: [ username: [
{ required: true, trigger: "blur", message: "请输入您的账号" }, { required: true, trigger: "blur", message: "请输入您的账号" },
{ min: 2, max: 20, message: '用户账号长度必须介于 2 和 20 之间', trigger: 'blur' } { min: 2, max: 20, message: '用户账号长度必须介于 2 和 20 之间', trigger: 'blur' }