feat: 权限缓存加载适配

This commit is contained in:
Ray.Hao
2025-12-12 22:26:43 +08:00
parent 0a594e2ce1
commit 3f05f77351
15 changed files with 210 additions and 91 deletions

View File

@@ -46,7 +46,6 @@ public class AuthController {
private final UserService userService;
private final TenantService tenantService;
private final TenantProperties tenantProperties;
private final PasswordEncoder passwordEncoder;
@Operation(summary = "获取验证码")
@GetMapping("/captcha")
@@ -75,8 +74,8 @@ public class AuthController {
return Result.success(authenticationToken);
}
// 多租户模式未指定租户ID查询该用户名在所有租户下的记录
List<User> users = userService.listUsersByUsername(username);
// 多租户模式未指定租户ID查询该用户名在所有租户下的账户
List<User> users = userService.findUserAcrossAllTenants(username);
if (users.isEmpty()) {
return Result.failed("用户不存在");

View File

@@ -48,18 +48,5 @@ public class TenantProperties {
*/
private String headerName = "tenant-id";
/**
* 初始化默认忽略的表
*/
public TenantProperties() {
// 系统表默认忽略多租户
ignoreTables.add("sys_tenant");
ignoreTables.add("sys_dict");
ignoreTables.add("sys_dict_item");
ignoreTables.add("sys_config");
// 代码生成表(平台共用,不做租户隔离)
ignoreTables.add("gen_table");
ignoreTables.add("gen_table_column");
}
}

View File

@@ -3,6 +3,8 @@ package com.youlai.boot.security.service;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.tenant.TenantContextHolder;
import com.youlai.boot.config.property.TenantProperties;
import com.youlai.boot.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -24,6 +26,7 @@ import java.util.*;
public class PermissionService {
private final RedisTemplate<String, Object> redisTemplate;
private final TenantProperties tenantProperties;
/**
* 判断当前登录用户是否拥有操作权限
@@ -67,21 +70,36 @@ public class PermissionService {
/**
* 从缓存中获取角色权限列表
* 构建租户权限缓存key
*
* @param tenantId 租户ID
* @return 缓存key
*/
private String buildRolePermsCacheKey(Long tenantId) {
if (!tenantProperties.getEnabled() || tenantId == null) {
return RedisConstants.System.ROLE_PERMS;
}
return RedisConstants.System.ROLE_PERMS + ":" + tenantId;
}
/**
* 从缓存中获取角色权限列表(兼容单租户和多租户)
*
* @param roleCodes 角色编码集合
* @return 角色权限列表
*/
public Set<String> getRolePermsFormCache(Set<String> roleCodes) {
// 检查输入是否为空
if (CollectionUtil.isEmpty(roleCodes)) {
return Collections.emptySet();
}
// 获取当前租户ID并构建缓存Key
Long tenantId = TenantContextHolder.getTenantId();
String cacheKey = buildRolePermsCacheKey(tenantId);
Set<String> perms = new HashSet<>();
// 从缓存中一次性获取所有角色的权限
Collection<Object> roleCodesAsObjects = new ArrayList<>(roleCodes);
List<Object> rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects);
List<Object> rolePermsList = redisTemplate.opsForHash().multiGet(cacheKey, roleCodesAsObjects);
for (Object rolePermsObj : rolePermsList) {
if (rolePermsObj instanceof Set) {

View File

@@ -43,12 +43,12 @@ public class TenantController {
*
* @return 租户列表
*/
@Operation(summary = "获取当前用户的租户列表")
@Operation(summary = "获取当前用户可访问的租户列表")
@GetMapping
public Result<List<TenantVO>> getTenantList() {
public Result<List<TenantVO>> getAccessibleTenants() {
Long userId = SecurityUtils.getUserId();
List<TenantVO> tenantList = tenantService.getTenantListByUserId(userId);
log.info("获取用户 {} 的租户列表,共 {} 个租户", userId, tenantList.size());
List<TenantVO> tenantList = tenantService.getAccessibleTenants(userId);
log.debug("用户 {} 可访问 {} 个租户", userId, tenantList.size());
return Result.success(tenantList);
}
@@ -88,11 +88,10 @@ public class TenantController {
log.info("用户 {} 请求切换租户:{} -> {}", userId, fromTenantId, tenantId);
// 验证用户是否有权限访问该租户
boolean hasPermission = tenantService.hasTenantPermission(userId, tenantId);
if (!hasPermission) {
log.warn("用户 {} 无权访问租户 {}", userId, tenantId);
return Result.failed("无权限访问该租户");
// 验证用户是否可以访问该租户
if (!tenantService.canAccessTenant(userId, tenantId)) {
log.warn("用户 {} 无权访问租户 {}", userId, tenantId);
return Result.failed("无权访问租户");
}
// 验证租户是否存在且正常

View File

@@ -13,6 +13,11 @@ import java.util.Set;
@Data
public class RolePermsBO {
/**
* 租户ID
*/
private Long tenantId;
/**
* 角色编码
*/

View File

@@ -18,11 +18,13 @@ public class User extends BaseEntity {
*/
private String username;
/**
* 昵称
*/
private String nickname;
/**
* 性别((1-男 2-女 0-保密)
*/

View File

@@ -15,12 +15,15 @@ import java.util.List;
public interface TenantService extends IService<Tenant> {
/**
* 根据用户ID查询用户所属的租户列表
* 获取用户可访问的租户列表
* <p>
* 通过用户名查询该用户在所有租户下的账户,返回可访问的租户列表
* </p>
*
* @param userId 用户ID
* @return 租户列表
* @return 可访问的租户列表
*/
List<TenantVO> getTenantListByUserId(Long userId);
List<TenantVO> getAccessibleTenants(Long userId);
/**
* 根据租户ID查询租户信息
@@ -39,11 +42,14 @@ public interface TenantService extends IService<Tenant> {
Long getTenantIdByDomain(String domain);
/**
* 验证用户是否有权限访问指定租户
* 检查用户是否可以访问指定租户
* <p>
* 验证该用户名在目标租户下是否存在账户
* </p>
*
* @param userId 用户ID
* @param tenantId 租户ID
* @return true-有权限false-无权限
* @return true-可访问false-不可访问
*/
boolean hasTenantPermission(Long userId, Long tenantId);
boolean canAccessTenant(Long userId, Long tenantId);
}

View File

@@ -83,12 +83,15 @@ public interface UserService extends IService<User> {
UserAuthCredentials getAuthCredentialsByUsernameAndTenant(String username, Long tenantId);
/**
* 根据用户名查询用户在所有租户下的记录(用于多租户登录时判断是否需要选择租户)
* 跨租户查询用户账户列表
* <p>
* 查询该用户名在所有租户下的账户记录,用于多租户登录时判断是否需要选择租户
* </p>
*
* @param username 用户名
* @return 用户列表(每个租户一条记录)
* @return 用户账户列表(每个租户一条记录)
*/
List<User> listUsersByUsername(String username);
List<User> findUserAcrossAllTenants(String username);
/**

View File

@@ -3,10 +3,11 @@ package com.youlai.boot.system.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.tenant.TenantContextHolder;
import com.youlai.boot.config.property.TenantProperties;
import com.youlai.boot.system.mapper.RoleMenuMapper;
import com.youlai.boot.system.model.bo.RolePermsBO;
import com.youlai.boot.system.model.entity.RoleMenu;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.system.service.RoleMenuService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
@@ -18,7 +19,7 @@ import java.util.List;
import java.util.Set;
/**
* 角色菜单服务实现类
* 角色菜单服务实现类(多租户优化版)
*
* @author Ray.Hao
* @since 2.5.0
@@ -29,76 +30,159 @@ import java.util.Set;
public class RoleMenuServiceImpl extends ServiceImpl<RoleMenuMapper, RoleMenu> implements RoleMenuService {
private final RedisTemplate<String, Object> redisTemplate;
private final TenantProperties tenantProperties;
/**
* 初始化权限缓存
* 构建租户权限缓存key
*
* @param tenantId 租户ID
* @return 缓存key
* - 多租户开启: system:role:perms:{tenantId}
* - 多租户关闭: system:role:perms
*/
@PostConstruct
public void initRolePermsCache() {
log.info("初始化权限缓存... ");
refreshRolePermsCache();
private String buildRolePermsCacheKey(Long tenantId) {
// 判断是否启用多租户
if (!tenantProperties.getEnabled() || tenantId == null) {
// 单租户模式或多租户未开启使用原有Key
return RedisConstants.System.ROLE_PERMS;
}
// 多租户模式开启Key按租户隔离
return RedisConstants.System.ROLE_PERMS + ":" + tenantId;
}
/**
* 刷新权限缓存
* 启动时初始化权限缓存
*/
@PostConstruct
public void initRolePermsCache() {
log.info("开始初始化权限缓存...");
List<RolePermsBO> allRolePermsList = this.baseMapper.getRolePermsList(null);
if (CollectionUtil.isEmpty(allRolePermsList)) {
log.warn("权限数据为空,跳过缓存初始化");
return;
}
if (tenantProperties.getEnabled()) {
// 多租户模式:按租户分组缓存
allRolePermsList.forEach(rolePerms -> {
Long tenantId = rolePerms.getTenantId();
if (tenantId == null) {
log.warn("多租户模式下,角色[{}]缺少tenantId跳过", rolePerms.getRoleCode());
return;
}
String cacheKey = RedisConstants.System.ROLE_PERMS + ":" + tenantId;
String roleCode = rolePerms.getRoleCode();
Set<String> perms = rolePerms.getPerms();
if (CollectionUtil.isNotEmpty(perms)) {
redisTemplate.opsForHash().put(cacheKey, roleCode, perms);
}
});
log.info("权限缓存初始化完成(多租户模式),共{}条数据", allRolePermsList.size());
} else {
// 单租户模式:所有数据统一缓存
String cacheKey = RedisConstants.System.ROLE_PERMS;
allRolePermsList.forEach(rolePerms -> {
String roleCode = rolePerms.getRoleCode();
Set<String> perms = rolePerms.getPerms();
if (CollectionUtil.isNotEmpty(perms)) {
redisTemplate.opsForHash().put(cacheKey, roleCode, perms);
}
});
log.info("权限缓存初始化完成(单租户模式),共{}条数据", allRolePermsList.size());
}
}
/**
* 刷新当前租户权限缓存
*/
@Override
public void refreshRolePermsCache() {
// 清理权限缓存
redisTemplate.opsForHash().delete(RedisConstants.System.ROLE_PERMS, "*");
Long tenantId = TenantContextHolder.getTenantId();
String cacheKey = buildRolePermsCacheKey(tenantId);
// 清理当前租户权限缓存
redisTemplate.delete(cacheKey);
// 重新加载当前租户权限
List<RolePermsBO> list = this.baseMapper.getRolePermsList(null);
if (CollectionUtil.isNotEmpty(list)) {
list.forEach(item -> {
String roleCode = item.getRoleCode();
Set<String> perms = item.getPerms();
if (CollectionUtil.isNotEmpty(perms)) {
redisTemplate.opsForHash().put(RedisConstants.System.ROLE_PERMS, roleCode, perms);
redisTemplate.opsForHash().put(cacheKey, roleCode, perms);
}
});
}
}
/**
* 刷新权限缓存
*/
@Override
public void refreshRolePermsCache(String roleCode) {
// 清理权限缓存
redisTemplate.opsForHash().delete(RedisConstants.System.ROLE_PERMS, roleCode);
List<RolePermsBO> list = this.baseMapper.getRolePermsList(roleCode);
if (CollectionUtil.isNotEmpty(list)) {
RolePermsBO rolePerms = list.get(0);
if (rolePerms == null) {
return;
}
Set<String> perms = rolePerms.getPerms();
if (CollectionUtil.isNotEmpty(perms)) {
redisTemplate.opsForHash().put(RedisConstants.System.ROLE_PERMS, roleCode, perms);
}
if (tenantId == null) {
log.info("权限缓存刷新完成(单租户模式)");
} else {
log.info("租户[{}]权限缓存刷新完成", tenantId);
}
}
/**
* 刷新权限缓存 (角色编码变更时调用)
* 刷新单个角色权限缓存
*/
@Override
public void refreshRolePermsCache(String roleCode) {
Long tenantId = TenantContextHolder.getTenantId();
String cacheKey = buildRolePermsCacheKey(tenantId);
// 清理指定角色缓存
redisTemplate.opsForHash().delete(cacheKey, roleCode);
// 重新加载指定角色权限
List<RolePermsBO> list = this.baseMapper.getRolePermsList(roleCode);
if (CollectionUtil.isNotEmpty(list)) {
RolePermsBO rolePerms = list.get(0);
if (rolePerms != null) {
Set<String> perms = rolePerms.getPerms();
if (CollectionUtil.isNotEmpty(perms)) {
redisTemplate.opsForHash().put(cacheKey, roleCode, perms);
}
}
}
if (tenantId == null) {
log.info("角色[{}]权限缓存刷新完成(单租户模式)", roleCode);
} else {
log.info("租户[{}]角色[{}]权限缓存刷新完成", tenantId, roleCode);
}
}
/**
* 刷新权限缓存(角色编码变更时调用)
*/
@Override
public void refreshRolePermsCache(String oldRoleCode, String newRoleCode) {
Long tenantId = TenantContextHolder.getTenantId();
String cacheKey = buildRolePermsCacheKey(tenantId);
// 清理旧角色权限缓存
redisTemplate.opsForHash().delete(RedisConstants.System.ROLE_PERMS, oldRoleCode);
redisTemplate.opsForHash().delete(cacheKey, oldRoleCode);
// 添加新角色权限缓存
List<RolePermsBO> list = this.baseMapper.getRolePermsList(newRoleCode);
if (CollectionUtil.isNotEmpty(list)) {
RolePermsBO rolePerms = list.get(0);
if (rolePerms == null) {
return;
if (rolePerms != null) {
Set<String> perms = rolePerms.getPerms();
if (CollectionUtil.isNotEmpty(perms)) {
redisTemplate.opsForHash().put(cacheKey, newRoleCode, perms);
}
}
Set<String> perms = rolePerms.getPerms();
redisTemplate.opsForHash().put(RedisConstants.System.ROLE_PERMS, newRoleCode, perms);
}
if (tenantId == null) {
log.info("角色编码变更: {} -> {},权限缓存已更新(单租户模式)", oldRoleCode, newRoleCode);
} else {
log.info("租户[{}]角色编码变更: {} -> {},权限缓存已更新", tenantId, oldRoleCode, newRoleCode);
}
}
@@ -110,6 +194,7 @@ public class RoleMenuServiceImpl extends ServiceImpl<RoleMenuMapper, RoleMenu> i
*/
@Override
public Set<String> getRolePermsByRoleCodes(Set<String> roles) {
// 直接查询数据库(保持原有逻辑)
return this.baseMapper.listRolePerms(roles);
}

View File

@@ -32,7 +32,7 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
private final UserMapper userMapper;
@Override
public List<TenantVO> getTenantListByUserId(Long userId) {
public List<TenantVO> getAccessibleTenants(Long userId) {
// 临时忽略租户过滤,查询所有租户
TenantContextHolder.setIgnoreTenant(true);
try {
@@ -123,7 +123,7 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
}
@Override
public boolean hasTenantPermission(Long userId, Long tenantId) {
public boolean canAccessTenant(Long userId, Long tenantId) {
TenantContextHolder.setIgnoreTenant(true);
try {
// 先根据用户ID查询用户信息获取 username

View File

@@ -271,15 +271,15 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
}
@Override
public List<User> listUsersByUsername(String username) {
// 临时忽略租户过滤,查询该用户名在所有租户下的记录
public List<User> findUserAcrossAllTenants(String username) {
// 临时忽略租户过滤,查询该用户名在所有租户下的账户记录
TenantContextHolder.setIgnoreTenant(true);
try {
return this.list(
new LambdaQueryWrapper<User>()
.eq(User::getUsername, username)
.eq(User::getIsDeleted, 0)
.orderByAsc(User::getTenantId) // 按租户ID排序优先返回较小的租户ID
.orderByAsc(User::getTenantId)
);
} finally {
TenantContextHolder.setIgnoreTenant(false);