完成部分权限的认证操作的迁移

This commit is contained in:
YunaiV
2021-01-23 22:03:06 +08:00
parent aa38c0f9d1
commit bbe71ec2c8
39 changed files with 370 additions and 394 deletions

View File

@ -233,11 +233,6 @@ public class OperateLogAspect {
}
}
private static void fillContentFields(SysOperateLogCreateReqVO operateLogVO) {
operateLogVO.setContent(CONTENT.get());
operateLogVO.setExts(EXTS.get());
}
private static boolean isLogEnable(ProceedingJoinPoint joinPoint, OperateLog operateLog) {
// 有 @OperateLog 注解的情况下
if (operateLog != null) {

View File

@ -2,7 +2,7 @@ package cn.iocoder.dashboard.framework.security.core.handler;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
import cn.iocoder.dashboard.framework.security.core.service.SecurityFrameworkService;
import cn.iocoder.dashboard.framework.security.core.service.SecurityAuthFrameworkService;
import cn.iocoder.dashboard.framework.security.core.util.SecurityUtils;
import cn.iocoder.dashboard.util.servlet.ServletUtils;
import org.springframework.security.core.Authentication;
@ -26,7 +26,7 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
private SecurityProperties securityProperties;
@Resource
private SecurityFrameworkService securityFrameworkService;
private SecurityAuthFrameworkService securityFrameworkService;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {

View File

@ -4,16 +4,11 @@ import cn.iocoder.dashboard.framework.security.core.LoginUser;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* Security 框架 Service 接口定义 security 组件需要的功能
* Security 框架 Auth Service 接口定义 security 组件需要的功能
*
* @author 芋道源码
*/
public interface SecurityFrameworkService extends UserDetailsService {
/**
* 基于 token 退出登录
*
* @param token token
*/
void logout(String token);
public interface SecurityAuthFrameworkService extends UserDetailsService {
/**
* 校验 token 的有效性并获取用户信息
@ -32,4 +27,11 @@ public interface SecurityFrameworkService extends UserDetailsService {
*/
LoginUser mockLogin(Long userId);
/**
* 基于 token 退出登录
*
* @param token token
*/
void logout(String token);
}

View File

@ -0,0 +1,26 @@
package cn.iocoder.dashboard.framework.security.core.service;
/**
* Security 框架 Permission Service 接口,定义 security 组件需要的功能
*
* @author 芋道源码
*/
public interface SecurityPermissionFrameworkService {
/**
* 判断是否有权限
*
* @param permission 权限
* @return 是否
*/
boolean hasPermission(String permission);
/**
* 判断是否有权限,任一一个即可
*
* @param permissions 权限
* @return 是否
*/
boolean hasAnyPermissions(String... permissions);
}

View File

@ -3,7 +3,9 @@ package cn.iocoder.dashboard.framework.web.core.handler;
import cn.iocoder.dashboard.common.exception.GlobalException;
import cn.iocoder.dashboard.common.exception.ServiceException;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.framework.security.core.util.SecurityUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
@ -64,6 +66,9 @@ public class GlobalExceptionHandler {
if (ex instanceof ServiceException) {
return serviceExceptionHandler((ServiceException) ex);
}
if (ex instanceof AccessDeniedException) {
return accessDeniedExceptionHandler(request, (AccessDeniedException) ex);
}
if (ex instanceof GlobalException) {
return globalExceptionHandler(request, (GlobalException) ex);
}
@ -131,7 +136,7 @@ public class GlobalExceptionHandler {
public CommonResult<?> validationException(ValidationException ex) {
log.warn("[constraintViolationExceptionHandler]", ex);
// 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读
return CommonResult.error(BAD_REQUEST.getCode(), "请求参数不正确");
return CommonResult.error(BAD_REQUEST);
}
/**
@ -158,6 +163,18 @@ public class GlobalExceptionHandler {
return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage()));
}
/**
* 处理 Spring Security 权限不足的异常
*
* 来源是,使用 @PreAuthorize 注解AOP 进行权限拦截
*/
@ExceptionHandler(value = AccessDeniedException.class)
public CommonResult<?> accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) {
log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", SecurityUtils.getLoginUserId(),
req.getRequestURL(), ex);
return CommonResult.error(FORBIDDEN);
}
/**
* 处理业务异常 ServiceException
*

View File

@ -7,9 +7,7 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@ -22,7 +20,7 @@ public class SysAuthLoginReqVO {
@ApiModelProperty(value = "账号", required = true, example = "yudaoyuanma")
@NotEmpty(message = "登陆账号不能为空")
@Length(min = 5, max = 16, message = "账号长度为 5-16 位")
@Length(min = 4, max = 16, message = "账号长度为 4-16 位")
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
private String username;

View File

@ -0,0 +1,3 @@
### 请求 /system/user/page 接口 => 没有权限
GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10
Authorization: Bearer test104 # 使用测试账号

View File

@ -18,6 +18,7 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@ -41,7 +42,7 @@ public class SysUserController {
@ApiOperation("获得用户分页列表")
@GetMapping("/page")
// @PreAuthorize("@ss.hasPermi('system:user:list')")
@PreAuthorize("@ss.hasPermission('system:user:list')")
public CommonResult<PageResult<SysUserPageItemRespVO>> pageUsers(@Validated SysUserPageReqVO reqVO) {
// 获得用户分页列表
PageResult<SysUserDO> pageResult = userService.pageUsers(reqVO);

View File

@ -1,16 +1,17 @@
package cn.iocoder.dashboard.modules.system.dal.mysql.dao.permission;
import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRoleMenuDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Mapper
public interface SysRoleMenuMapper extends BaseMapper<SysRoleMenuDO> {
public interface SysRoleMenuMapper extends BaseMapperX<SysRoleMenuDO> {
default List<SysRoleMenuDO> selectListByRoleId(Long roleId) {
return selectList(new QueryWrapper<SysRoleMenuDO>().eq("role_id", roleId));
@ -32,4 +33,9 @@ public interface SysRoleMenuMapper extends BaseMapper<SysRoleMenuDO> {
.in("menu_id", menuIds));
}
default boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime) {
return selectOne(new QueryWrapper<SysRoleMenuDO>().select("id")
.gt("update_time", maxUpdateTime).last("LIMIT 1")) != null;
}
}

View File

@ -0,0 +1,29 @@
package cn.iocoder.dashboard.modules.system.mq.consumer.permission;
import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener;
import cn.iocoder.dashboard.modules.system.mq.message.permission.SysRoleMenuRefreshMessage;
import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 针对 {@link SysRoleMenuRefreshMessage} 的消费者
*
* @author 芋道源码
*/
@Component
@Slf4j
public class SysRoleMenuRefreshConsumer extends AbstractChannelMessageListener<SysRoleMenuRefreshMessage> {
@Resource
private SysPermissionService permissionService;
@Override
public void onMessage(SysRoleMenuRefreshMessage message) {
log.info("[onMessage][收到 Role 与 Menu 的关联刷新消息]");
permissionService.initLocalCache();
}
}

View File

@ -0,0 +1,17 @@
package cn.iocoder.dashboard.modules.system.mq.message.permission;
import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage;
import lombok.Data;
/**
* 角色与菜单数据刷新 Message
*/
@Data
public class SysRoleMenuRefreshMessage implements ChannelMessage {
@Override
public String getChannel() {
return "system.role-menu.refresh";
}
}

View File

@ -0,0 +1,27 @@
package cn.iocoder.dashboard.modules.system.mq.producer.permission;
import cn.iocoder.dashboard.framework.redis.core.util.RedisMessageUtils;
import cn.iocoder.dashboard.modules.system.mq.message.permission.SysRoleMenuRefreshMessage;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* Permission 权限相关消息的 Producer
*/
@Component
public class SysPermissionProducer {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 发送 {@link SysRoleMenuRefreshMessage} 消息
*/
public void sendRoleMenuRefreshMessage() {
SysRoleMenuRefreshMessage message = new SysRoleMenuRefreshMessage();
RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message);
}
}

View File

@ -1,6 +1,6 @@
package cn.iocoder.dashboard.modules.system.service.auth;
import cn.iocoder.dashboard.framework.security.core.service.SecurityFrameworkService;
import cn.iocoder.dashboard.framework.security.core.service.SecurityAuthFrameworkService;
/**
* 认证 Service 接口
@ -9,7 +9,7 @@ import cn.iocoder.dashboard.framework.security.core.service.SecurityFrameworkSer
*
* @author 芋道源码
*/
public interface SysAuthService extends SecurityFrameworkService {
public interface SysAuthService extends SecurityAuthFrameworkService {
String login(String username, String password, String captchaUUID, String captchaCode);

View File

@ -59,6 +59,14 @@ public interface SysMenuService {
List<SysMenuDO> listMenusFromCache(Collection<Long> menuIds, Collection<Integer> menuTypes,
Collection<Integer> menusStatuses);
/**
* 获得权限对应的菜单数组
*
* @param permission 权限标识
* @return 数组
*/
List<SysMenuDO> getMenuListByPermissionFromCache(String permission);
/*
* 创建菜单
*

View File

@ -1,5 +1,6 @@
package cn.iocoder.dashboard.modules.system.service.permission;
import cn.iocoder.dashboard.framework.security.core.service.SecurityPermissionFrameworkService;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysMenuDO;
import org.springframework.lang.Nullable;
@ -14,12 +15,12 @@ import java.util.Set;
*
* @author 芋道源码
*/
public interface SysPermissionService {
public interface SysPermissionService extends SecurityPermissionFrameworkService {
/**
* 初始化
*/
void init();
void initLocalCache();
/**
* 获得角色们拥有的菜单列表,从缓存中获取

View File

@ -56,6 +56,16 @@ public interface SysRoleService {
*/
boolean hasAnyAdmin(Collection<SysRoleDO> roleList);
/**
* 判断角色编号数组中,是否有管理员
*
* @param ids 角色编号数组
* @return 是否有管理员
*/
default boolean hasAnyAdmin(Set<Long> ids) {
return hasAnyAdmin(listRolesFromCache(ids));
}
/**
* 创建角色
*

View File

@ -61,7 +61,7 @@ public class SysMenuServiceImpl implements SysMenuService {
*
* 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向
*/
private volatile Multimap<String, SysMenuDO> permMenuCache;
private volatile Multimap<String, SysMenuDO> permissionMenuCache;
/**
* 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
*/
@ -76,7 +76,7 @@ public class SysMenuServiceImpl implements SysMenuService {
private SysMenuProducer menuProducer;
/**
* 初始化 {@link #menuCache} 和 {@link #permMenuCache} 缓存
* 初始化 {@link #menuCache} 和 {@link #permissionMenuCache} 缓存
*/
@Override
@PostConstruct
@ -95,7 +95,7 @@ public class SysMenuServiceImpl implements SysMenuService {
permMenuCacheBuilder.put(menuDO.getPermission(), menuDO);
});
menuCache = menuCacheBuilder.build();
permMenuCache = permMenuCacheBuilder.build();
permissionMenuCache = permMenuCacheBuilder.build();
assert menuList.size() > 0; // 断言,避免告警
maxUpdateTime = menuList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
log.info("[initLocalCache][缓存菜单,数量为:{}]", menuList.size());
@ -162,6 +162,11 @@ public class SysMenuServiceImpl implements SysMenuService {
.collect(Collectors.toList());
}
@Override
public List<SysMenuDO> getMenuListByPermissionFromCache(String permission) {
return new ArrayList<>(permissionMenuCache.get(permission));
}
@Override
public Long createMenu(SysMenuCreateReqVO reqVO) {
// 校验父菜单存在

View File

@ -2,12 +2,16 @@ package cn.iocoder.dashboard.modules.system.service.permission.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.dashboard.framework.security.core.util.SecurityUtils;
import cn.iocoder.dashboard.modules.system.dal.mysql.dao.permission.SysRoleMenuMapper;
import cn.iocoder.dashboard.modules.system.dal.mysql.dao.permission.SysUserRoleMapper;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysMenuDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRoleDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRoleMenuDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysUserRoleDO;
import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysPermissionProducer;
import cn.iocoder.dashboard.modules.system.service.permission.SysMenuService;
import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
import cn.iocoder.dashboard.modules.system.service.permission.SysRoleService;
@ -18,23 +22,28 @@ import com.google.common.collect.Multimap;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.*;
/**
* 权限 Service 实现类
*
* @author 芋道源码
*/
@Service
@Service("ss") // 使用 Spring Security 的缩写,方便食用
@Slf4j
public class SysPermissionServiceImpl implements SysPermissionService {
/**
* 定时执行 {@link #schedulePeriodicRefresh()} 的周期
* 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
*/
private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
/**
* 角色编号与菜单编号的缓存映射
* key角色编号
@ -51,6 +60,10 @@ public class SysPermissionServiceImpl implements SysPermissionService {
* 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向
*/
private volatile Multimap<Long, Long> menuRoleCache;
/**
* 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
*/
private volatile Date maxUpdateTime;
@Resource
private SysRoleMenuMapper roleMenuMapper;
@ -62,14 +75,22 @@ public class SysPermissionServiceImpl implements SysPermissionService {
@Resource
private SysMenuService menuService;
@Resource
private SysPermissionProducer permissionProducer;
/**
* 初始化 {@link #roleMenuCache} 和 {@link #menuRoleCache} 缓存
*/
@Override
@PostConstruct
public void init() {
public void initLocalCache() {
// 获取角色与菜单的关联列表,如果有更新
List<SysRoleMenuDO> roleMenuList = this.loadRoleMenuIfUpdate(maxUpdateTime);
if (CollUtil.isEmpty(roleMenuList)) {
return;
}
// 初始化 roleMenuCache 和 menuRoleCache 缓存
List<SysRoleMenuDO> roleMenuList = roleMenuMapper.selectList(null);
ImmutableMultimap.Builder<Long, Long> roleMenuCacheBuilder = ImmutableMultimap.builder();
ImmutableMultimap.Builder<Long, Long> menuRoleCacheBuilder = ImmutableMultimap.builder();
roleMenuList.forEach(roleMenuDO -> {
@ -78,9 +99,32 @@ public class SysPermissionServiceImpl implements SysPermissionService {
});
roleMenuCache = roleMenuCacheBuilder.build();
menuRoleCache = menuRoleCacheBuilder.build();
assert roleMenuList.size() > 0; // 断言,避免告警
maxUpdateTime = roleMenuList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
log.info("[initLocalCache][初始化角色与菜单的关联数量为 {}]", roleMenuList.size());
}
/**
* 如果角色与菜单的关联发生变化,从数据库中获取最新的全量角色与菜单的关联。
* 如果未发生变化,则返回空
*
* @param maxUpdateTime 当前角色与菜单的关联的最大更新时间
* @return 角色与菜单的关联列表
*/
private List<SysRoleMenuDO> loadRoleMenuIfUpdate(Date maxUpdateTime) {
// 第一步,判断是否要更新。
if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
log.info("[loadRoleMenuIfUpdate][首次加载全量角色与菜单的关联]");
} else { // 判断数据库中是否有更新的角色与菜单的关联
if (!roleMenuMapper.selectExistsByUpdateTimeAfter(maxUpdateTime)) {
return null;
}
log.info("[loadRoleMenuIfUpdate][增量加载全量角色与菜单的关联]");
}
// 第二步,如果有更新,则从数据库加载所有角色与菜单的关联
return roleMenuMapper.selectList();
}
@Override
public List<SysMenuDO> listRoleMenusFromCache(Collection<Long> roleIds, Collection<Integer> menuTypes,
Collection<Integer> menusStatuses) {
@ -140,6 +184,15 @@ public class SysPermissionServiceImpl implements SysPermissionService {
if (!CollectionUtil.isEmpty(deleteMenuIds)) {
roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds);
}
// 发送刷新消息. 注意,需要事务提交后,在进行发送刷新消息。不然 db 还未提交,结果缓存先刷新了
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
permissionProducer.sendRoleMenuRefreshMessage();
}
});
}
@Override
@ -189,4 +242,39 @@ public class SysPermissionServiceImpl implements SysPermissionService {
// TODO 实现我
}
@Override
public boolean hasPermission(String permission) {
return hasAnyPermissions(permission);
}
@Override
public boolean hasAnyPermissions(String... permissions) {
// 如果为空,说明已经有权限
if (ArrayUtil.isEmpty(permissions)) {
return true;
}
// 获得当前登陆的角色。如果为空,说明没有权限
Set<Long> roleIds = SecurityUtils.getLoginUserRoleIds();
if (CollUtil.isEmpty(roleIds)) {
return false;
}
// 判断是否是超管。如果是,当然符合条件
if (roleService.hasAnyAdmin(roleIds)) {
return true;
}
// 遍历权限,判断是否有一个满足
return Arrays.stream(permissions).anyMatch(permission -> {
List<SysMenuDO> menuList = menuService.getMenuListByPermissionFromCache(permission);
// 采用严格模式,如果权限找不到对应的 Menu 的话,认为
if (CollUtil.isEmpty(menuList)) {
return false;
}
// 获得是否拥有该权限,任一一个
return menuList.stream().anyMatch(menu -> CollUtil.containsAny(roleIds,
menuRoleCache.get(menu.getId())));
});
}
}

View File

@ -217,6 +217,8 @@ public class SysRoleServiceImpl implements SysRoleService {
updateObject.setId(id);
updateObject.setStatus(status);
roleMapper.updateById(updateObject);
// 发送刷新消息
roleProducer.sendRoleRefreshMessage();
}
@Override
@ -229,6 +231,8 @@ public class SysRoleServiceImpl implements SysRoleService {
updateObject.setDataScope(dataScope);
updateObject.setDataScopeDeptIds(dataScopeDeptIds);
roleMapper.updateById(updateObject);
// 发送刷新消息
roleProducer.sendRoleRefreshMessage();
}
/**

View File

@ -34,7 +34,7 @@ yudao:
token-timeout: 1d
session-timeout: 30m
mock-enable: true
mock-secret: yudaoyuanma
mock-secret: test
swagger:
title: 管理后台
description: 提供管理员管理的所有功能