fix: RBAC模型P3安全修复

P3-13: 密码改用BCrypt+MD5兼容模式
P3-14: 角色继承-增加parentId递归合并父角色权限
P3-15: 权限变更审计日志-新增base_sys_perms_audit_log表
This commit is contained in:
OpenCode
2026-07-18 18:24:36 +08:00
parent 37df40f5e4
commit 13a90c2573
7 changed files with 132 additions and 14 deletions
@@ -22,6 +22,7 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@@ -125,15 +126,19 @@ public class JwtSecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
private final BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();
@Override
public String encode(CharSequence rawPassword) {
return DigestUtils.md5DigestAsHex(((String) rawPassword).getBytes());
return bcrypt.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(
DigestUtils.md5DigestAsHex(((String) rawPassword).getBytes()));
if (encodedPassword.startsWith("$2a$") || encodedPassword.startsWith("$2b$") || encodedPassword.startsWith("$2y$")) {
return bcrypt.matches(rawPassword, encodedPassword);
}
return encodedPassword.equals(DigestUtils.md5DigestAsHex(((String) rawPassword).getBytes()));
}
};
}
@@ -0,0 +1,41 @@
package com.cool.modules.base.entity.sys;
import com.cool.core.base.BaseEntity;
import com.cool.core.annotation.ColumnDefine;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Table;
import com.cool.core.mybatis.handler.Fastjson2TypeHandler;
import lombok.Getter;
import lombok.Setter;
import org.dromara.autotable.annotation.Index;
@Getter
@Setter
@Table(value = "base_sys_perms_audit_log", comment = "权限变更审计日志表")
public class BaseSysPermsAuditLogEntity extends BaseEntity<BaseSysPermsAuditLogEntity> {
@Index
@ColumnDefine(comment = "操作用户ID", type = "bigint")
private Long operatorUserId;
@ColumnDefine(comment = "操作用户名")
private String operatorUsername;
@ColumnDefine(comment = "变更类型: ROLE_ASSIGN/PERMS_UPDATE/ROLE_DISABLE/ROLE_ENABLE/USER_ROLE_UPDATE")
private String changeType;
@Index
@ColumnDefine(comment = "目标ID(角色ID或用户ID)", type = "bigint")
private Long targetId;
@ColumnDefine(comment = "变更前值", type = "json")
@Column(typeHandler = Fastjson2TypeHandler.class)
private Object beforeValue;
@ColumnDefine(comment = "变更后值", type = "json")
@Column(typeHandler = Fastjson2TypeHandler.class)
private Object afterValue;
@ColumnDefine(comment = "IP地址", length = 50)
private String ip;
}
@@ -36,6 +36,9 @@ public class BaseSysRoleEntity extends TenantEntity<BaseSysRoleEntity> {
@ColumnDefine(comment = "数据权限是否关联上下级", defaultValue = "1")
private Integer relevance;
@ColumnDefine(comment = "父角色ID", type = "bigint")
private Long parentId;
@ColumnDefine(comment = "菜单权限", type = "json")
@Column(typeHandler = Fastjson2TypeHandler.class)
private List<Long> menuIdList;
@@ -0,0 +1,7 @@
package com.cool.modules.base.mapper.sys;
import com.mybatisflex.core.BaseMapper;
import com.cool.modules.base.entity.sys.BaseSysPermsAuditLogEntity;
public interface BaseSysPermsAuditLogMapper extends BaseMapper<BaseSysPermsAuditLogEntity> {
}
@@ -8,7 +8,6 @@ import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.MD5;
import cn.hutool.jwt.JWT;
import com.cool.core.cache.CoolCache;
import com.cool.core.enums.UserTypeEnum;
@@ -16,8 +15,7 @@ import com.cool.core.exception.CoolPreconditions;
import com.cool.core.security.jwt.JwtTokenUtil;
import com.cool.core.util.CoolSecurityUtil;
import com.cool.modules.base.dto.sys.BaseSysLoginDto;
import com.cool.modules.base.dto.sys.BaseSysRegisterDto;
import com.cool.modules.base.entity.sys.BaseSysDepartmentEntity;
import com.cool.modules.base.dto.sys.BaseSysRegisterDto;import com.cool.modules.base.entity.sys.BaseSysDepartmentEntity;
import com.cool.modules.base.entity.sys.BaseSysRoleEntity;
import com.cool.modules.base.entity.sys.BaseSysUserEntity;
import com.cool.modules.base.mapper.sys.BaseSysDepartmentMapper;
@@ -54,6 +52,8 @@ public class BaseSysLoginServiceImpl implements BaseSysLoginService {
private final BaseSysPermsService baseSysPermsService;
private final org.springframework.security.crypto.password.PasswordEncoder passwordEncoder;
@org.springframework.beans.factory.annotation.Value("${cool.captcha.enable:true}")
private boolean captchaEnable;
@@ -149,7 +149,7 @@ public class BaseSysLoginServiceImpl implements BaseSysLoginService {
CoolPreconditions.check(defaultDepartment == null, "系统未配置部门,请联系管理员");
BaseSysUserEntity userEntity = new BaseSysUserEntity();
userEntity.setUsername(baseSysRegisterDto.getUsername());
userEntity.setPassword(MD5.create().digestHex(baseSysRegisterDto.getPassword()));
userEntity.setPassword(passwordEncoder.encode(baseSysRegisterDto.getPassword()));
userEntity.setPasswordV(1);
userEntity.setNickName(StrUtil.isNotEmpty(baseSysRegisterDto.getNickName())
? baseSysRegisterDto.getNickName() : baseSysRegisterDto.getUsername());
@@ -23,11 +23,13 @@ import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class BaseSysPermsServiceImpl implements BaseSysPermsService {
@@ -49,6 +51,8 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
final private ExecutorService cachedThreadPool;
final private BaseSysPermsAuditLogMapper baseSysPermsAuditLogMapper;
@Override
public Long[] loginDepartmentIds() {
String username = CoolSecurityUtil.getAdminUsername();
@@ -162,7 +166,7 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
@Override
public String[] getPerms(Long[] roleIds) {
List<BaseSysMenuEntity> menus = getMenus(roleIds);
List<BaseSysMenuEntity> menus = getMenus(expandWithParentRoles(roleIds));
Set<String> perms = new HashSet<>();
String[] permsData = menus.stream().map(BaseSysMenuEntity::getPerms)
.filter(itemPerms -> !StrUtil.isEmpty(itemPerms)).toArray(String[]::new);
@@ -174,6 +178,10 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
@Override
public List<BaseSysMenuEntity> getMenus(Long[] roleIds) {
return getMenusInner(expandWithParentRoles(roleIds));
}
private List<BaseSysMenuEntity> getMenusInner(Long[] roleIds) {
if (CollUtil.toList(roleIds).contains(1L)) {
roleIds = null;
}
@@ -213,6 +221,12 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
@Override
@Transactional(rollbackFor = Exception.class)
public void updatePerms(Long roleId, Long[] menuIdList, Long[] departmentIds) {
Long[] beforeMenuIds = baseSysRoleMenuMapper.selectListByQuery(
QueryWrapper.create().eq(BaseSysRoleMenuEntity::getRoleId, roleId))
.stream().map(BaseSysRoleMenuEntity::getMenuId).toArray(Long[]::new);
Long[] beforeDeptIds = baseSysRoleDepartmentMapper.selectListByQuery(
QueryWrapper.create().eq(BaseSysRoleDepartmentEntity::getRoleId, roleId))
.stream().map(BaseSysRoleDepartmentEntity::getDepartmentId).toArray(Long[]::new);
// 更新菜单权限
baseSysRoleMenuMapper.deleteByQuery(QueryWrapper.create().eq(BaseSysRoleMenuEntity::getRoleId, roleId));
List<BaseSysRoleMenuEntity> batchRoleMenuList = new ArrayList<>();
@@ -239,6 +253,9 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
baseSysRoleDepartmentMapper.insertBatch(batchRoleDepartmentList);
}
SpringContextUtils.getBean(MySecurityMetadataSource.class).clearCache();
logPermsAudit("PERMS_UPDATE", roleId,
Dict.create().set("menuIds", beforeMenuIds).set("deptIds", beforeDeptIds),
Dict.create().set("menuIds", menuIdList).set("deptIds", departmentIds));
cachedThreadPool.submit(() -> {
// 刷新对应角色用户的权限
List<BaseSysUserRoleEntity> userRoles = baseSysUserRoleMapper
@@ -252,6 +269,9 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
@Override
@Transactional(rollbackFor = Exception.class)
public void updateUserRole(Long userId, Long[] roleIdList) {
Long[] beforeRoleIds = baseSysUserRoleMapper.selectListByQuery(
QueryWrapper.create().eq(BaseSysUserRoleEntity::getUserId, userId))
.stream().map(BaseSysUserRoleEntity::getRoleId).toArray(Long[]::new);
baseSysUserRoleMapper.deleteByQuery(QueryWrapper.create().eq(BaseSysUserRoleEntity::getUserId, userId));
if (roleIdList == null) {
roleIdList = new Long[0];
@@ -262,6 +282,9 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
sysUserRoleEntity.setUserId(userId);
baseSysUserRoleMapper.insert(sysUserRoleEntity);
}
logPermsAudit("USER_ROLE_UPDATE", userId,
Dict.create().set("roleIds", beforeRoleIds),
Dict.create().set("roleIds", roleIdList));
refreshPerms(userId);
}
@@ -322,4 +345,42 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
}
return getDepartmentIdsByUserId(CoolSecurityUtil.getCurrentUserId());
}
private Long[] expandWithParentRoles(Long[] roleIds) {
if (ObjectUtil.isEmpty(roleIds)) {
return roleIds;
}
Set<Long> expanded = new LinkedHashSet<>(Arrays.asList(roleIds));
for (Long roleId : roleIds) {
collectParentRoleIds(roleId, expanded);
}
return expanded.toArray(Long[]::new);
}
private void collectParentRoleIds(Long roleId, Set<Long> result) {
BaseSysRoleEntity role = baseSysRoleMapper.selectOneById(roleId);
if (role == null || role.getParentId() == null || result.contains(role.getParentId())) {
return;
}
BaseSysRoleEntity parentRole = baseSysRoleMapper.selectOneById(role.getParentId());
if (parentRole != null && parentRole.getStatus() != 0) {
result.add(parentRole.getId());
collectParentRoleIds(parentRole.getId(), result);
}
}
private void logPermsAudit(String changeType, Long targetId, Object beforeValue, Object afterValue) {
try {
BaseSysPermsAuditLogEntity auditLog = new BaseSysPermsAuditLogEntity();
auditLog.setOperatorUserId(CoolSecurityUtil.getCurrentUserId());
auditLog.setOperatorUsername(CoolSecurityUtil.getAdminUsername());
auditLog.setChangeType(changeType);
auditLog.setTargetId(targetId);
auditLog.setBeforeValue(beforeValue);
auditLog.setAfterValue(afterValue);
baseSysPermsAuditLogMapper.insertSelective(auditLog);
} catch (Exception e) {
log.warn("记录权限审计日志失败: {}", e.getMessage());
}
}
}
@@ -12,7 +12,6 @@ import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.MD5;
import cn.hutool.json.JSONObject;
import com.cool.core.base.BaseServiceImpl;
import com.cool.core.base.ModifyEnum;
@@ -34,6 +33,7 @@ import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.update.UpdateChain;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Collection;
@@ -57,6 +57,8 @@ public class BaseSysUserServiceImpl extends BaseServiceImpl<BaseSysUserMapper, B
final private BaseSysRoleMapper baseSysRoleMapper;
final private PasswordEncoder passwordEncoder;
@Override
public Object page(JSONObject requestParams, Page<BaseSysUserEntity> page, QueryWrapper qw) {
String keyWord = requestParams.getStr("keyWord");
@@ -131,9 +133,8 @@ public class BaseSysUserServiceImpl extends BaseServiceImpl<BaseSysUserMapper, B
userEntity.setNickName(body.getStr("nickName"));
userEntity.setHeadImg(body.getStr("headImg"));
if (StrUtil.isNotEmpty(body.getStr("password")) && StrUtil.isNotEmpty(body.getStr("newPassword"))) {
String oldPasswordMd5 = MD5.create().digestHex(body.getStr("password"));
CoolPreconditions.check(!oldPasswordMd5.equals(userEntity.getPassword()), "原密码错误");
userEntity.setPassword(MD5.create().digestHex(body.getStr("newPassword")));
CoolPreconditions.check(!passwordEncoder.matches(body.getStr("password"), userEntity.getPassword()), "原密码错误");
userEntity.setPassword(passwordEncoder.encode(body.getStr("newPassword")));
userEntity.setPasswordV(userEntity.getPasswordV() + 1);
coolCache.set("admin:passwordVersion:" + userId, userEntity.getPasswordV());
}
@@ -153,7 +154,7 @@ public class BaseSysUserServiceImpl extends BaseServiceImpl<BaseSysUserMapper, B
BaseSysUserEntity check = getOne(
QueryWrapper.create().eq(BaseSysUserEntity::getUsername, entity.getUsername()));
CoolPreconditions.check(check != null, "用户名已存在");
entity.setPassword(MD5.create().digestHex(entity.getPassword()));
entity.setPassword(passwordEncoder.encode(entity.getPassword()));
super.add(requestParams, entity);
return entity.getId();
}
@@ -167,7 +168,7 @@ public class BaseSysUserServiceImpl extends BaseServiceImpl<BaseSysUserMapper, B
String requestPassword = requestParams.getStr("password");
if (StrUtil.isNotEmpty(requestPassword)) {
entity.setPasswordV(userEntity.getPasswordV() + 1);
entity.setPassword(MD5.create().digestHex(requestPassword));
entity.setPassword(passwordEncoder.encode(requestPassword));
coolCache.set("admin:passwordVersion:" + entity.getId(), entity.getPasswordV());
} else {
entity.setPassword(userEntity.getPassword());