优化菜单管理

This commit is contained in:
zhou-hao 2021-10-12 09:39:11 +08:00
parent 70b45ee12c
commit 2a96d6896d
7 changed files with 493 additions and 49 deletions

View File

@ -0,0 +1,47 @@
package org.jetlinks.community.auth.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.collections4.CollectionUtils;
import java.util.*;
import java.util.function.BiPredicate;
@Getter
@Setter
public class MenuButtonInfo {
@Schema(description = "按钮ID")
private String id;
@Schema(description = "按钮名称")
private String name;
@Schema(description = "权限信息")
private List<PermissionInfo> permissions;
@Schema(description = "其他配置")
private Map<String, Object> options;
public boolean hasPermission(BiPredicate<String, Collection<String>> predicate) {
if (CollectionUtils.isEmpty(permissions)) {
return true;
}
for (PermissionInfo permission : permissions) {
if (!predicate.test(permission.getPermission(), permission.getActions())) {
return false;
}
}
return true;
}
public static MenuButtonInfo of(String id, String name, String permission, String... actions) {
MenuButtonInfo info = new MenuButtonInfo();
info.setId(id);
info.setName(name);
info.setPermissions(Arrays.asList(PermissionInfo.of(permission, new HashSet<>(Arrays.asList(actions)))));
return info;
}
}

View File

@ -1,29 +1,41 @@
package org.jetlinks.community.auth.entity;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.collections4.CollectionUtils;
import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
import org.hswebframework.ezorm.rdb.mapping.annotation.Comment;
import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec;
import org.hswebframework.web.api.crud.entity.GenericTreeSortSupportEntity;
import org.jetlinks.community.auth.web.request.AuthorizationSettingDetail;
import javax.persistence.Column;
import javax.persistence.Index;
import javax.persistence.Table;
import java.sql.JDBCType;
import java.util.List;
import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* 菜单定义实体类
*
* @author wangzheng
* @since 1.0
*/
@Getter
@Setter
@Table(name = "s_menu", indexes = {
@Index(name = "idx_menu_path", columnList = "path")
@Index(name = "idx_menu_path", columnList = "path")
})
public class MenuEntity
extends GenericTreeSortSupportEntity<String> {
extends GenericTreeSortSupportEntity<String> {
@Schema(description = "名称")
@Comment("菜单名称")
@Column(length = 32)
private String name;
@ -31,29 +43,101 @@ public class MenuEntity
@Comment("描述")
@Column
@ColumnType(jdbcType = JDBCType.CLOB)
@Schema(description = "描述")
private String describe;
@Hidden
@Deprecated
@Comment("权限表达式")
@Column(name = "permission_expression", length = 256)
private String permissionExpression;
@Comment("菜单对应的url")
@Column(length = 512)
@Schema(description = "URL,路由")
private String url;
@Comment("图标")
@Column(length = 256)
@Schema(description = "图标")
private String icon;
@Comment("状态")
@Column
@ColumnType(jdbcType = JDBCType.SMALLINT)
@Schema(description = "状态,0为禁用,1为启用")
@DefaultValue("1")
private Byte status;
@Schema(description = "默认权限信息")
@Column
@JsonCodec
@ColumnType(jdbcType = JDBCType.LONGVARCHAR, javaType = String.class)
private List<PermissionInfo> permissions;
@Schema(description = "按钮定义信息")
@Column
@JsonCodec
@ColumnType(jdbcType = JDBCType.LONGVARCHAR, javaType = String.class)
private List<MenuButtonInfo> buttons;
@Schema(description = "其他配置信息")
@Column
@JsonCodec
@ColumnType(jdbcType = JDBCType.LONGVARCHAR, javaType = String.class)
private Map<String, Object> options;
//子菜单
@Schema(description = "子菜单")
private List<MenuEntity> children;
@Override
public List<MenuEntity> getChildren() {
return children;
public MenuEntity copy(Predicate<MenuButtonInfo> buttonPredicate) {
MenuEntity entity = this.copyTo(new MenuEntity());
if (CollectionUtils.isEmpty(entity.getButtons())) {
return entity;
}
entity.setButtons(
entity
.getButtons()
.stream()
.filter(buttonPredicate)
.collect(Collectors.toList())
);
return entity;
}
public boolean hasPermission(BiPredicate<String, Collection<String>> predicate) {
if (CollectionUtils.isEmpty(permissions) && CollectionUtils.isEmpty(buttons)) {
return false;
}
//有权限信息
if (CollectionUtils.isNotEmpty(permissions)) {
for (PermissionInfo permission : permissions) {
if (!predicate.test(permission.getPermission(), permission.getActions())) {
return false;
}
}
return true;
}
//有任意按钮信息
if (CollectionUtils.isNotEmpty(buttons)) {
for (MenuButtonInfo button : buttons) {
if (button.hasPermission(predicate)) {
return true;
}
}
}
return false;
}
public Optional<MenuButtonInfo> getButton(String id) {
if (buttons == null) {
return Optional.empty();
}
return buttons
.stream()
.filter(button -> Objects.equals(button.getId(), id))
.findAny();
}
}

View File

@ -0,0 +1,58 @@
package org.jetlinks.community.auth.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hswebframework.web.api.crud.entity.GenericTreeSortSupportEntity;
import org.hswebframework.web.bean.FastBeanCopier;
import java.util.List;
import java.util.Map;
@Getter
@Setter
public class MenuView extends GenericTreeSortSupportEntity<String> {
@Schema(description = "菜单名称")
private String name;
@Schema(description = "图标")
private String icon;
@Schema(description = "URL")
private String url;
@Schema(description = "父节点")
private String parentId;
@Schema(description = "按钮")
private List<ButtonView> buttons;
@Schema(description = "其他配置")
private Map<String, Object> options;
@Schema(description = "子节点")
private List<MenuView> children;
@Getter
@Setter
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public static class ButtonView{
@Schema(description = "按钮ID")
private String id;
@Schema(description = "按钮名称")
private String name;
@Schema(description = "其他配置")
private Map<String,Object> options;
}
public static MenuView of(MenuEntity entity){
return FastBeanCopier.copy(entity,new MenuView());
}
}

View File

@ -0,0 +1,23 @@
package org.jetlinks.community.auth.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Set;
@Getter
@Setter
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class PermissionInfo {
@Schema(description = "权限ID")
private String permission;
@Schema(description = "权限操作")
private Set<String> actions;
}

View File

@ -1,31 +1,37 @@
package org.jetlinks.community.auth.web;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.hswebframework.web.api.crud.entity.TreeSupportEntity;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.authorization.AuthenticationUtils;
import org.hswebframework.web.authorization.annotation.Authorize;
import org.hswebframework.web.authorization.annotation.Resource;
import org.hswebframework.web.authorization.annotation.ResourceAction;
import org.hswebframework.web.authorization.exception.UnAuthorizedException;
import org.hswebframework.web.crud.service.ReactiveCrudService;
import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
import org.jetlinks.community.auth.entity.MenuButtonInfo;
import org.jetlinks.community.auth.entity.MenuEntity;
import org.jetlinks.community.auth.entity.MenuView;
import org.jetlinks.community.auth.service.AuthorizationSettingDetailService;
import org.jetlinks.community.auth.service.DefaultMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.jetlinks.community.auth.web.request.MenuGrantRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* 菜单管理
*
* @author wangzheng
* @since 1.0
*/
@ -33,27 +39,140 @@ import java.util.stream.Collectors;
@RequestMapping("/menu")
@Authorize
@Resource(id = "menu", name = "菜单管理", group = "system")
@Hidden
@Tag(name = "菜单管理")
@AllArgsConstructor
public class MenuController implements ReactiveServiceCrudController<MenuEntity, String> {
@Autowired
private DefaultMenuService defaultMenuService;
private final DefaultMenuService defaultMenuService;
private final AuthorizationSettingDetailService settingService;
@Override
public ReactiveCrudService<MenuEntity, String> getService() {
return defaultMenuService;
}
public Collection<MenuEntity> predicateUserMenu(Map<String, MenuEntity> menuMap, Authentication autz) {
/**
* 获取用户自己的菜单列表
*
* @return 菜单列表
*/
@GetMapping("/user-own/tree")
@Authorize(merge = false)
@Operation(summary = "获取当前用户可访问的菜单(树结构)")
public Flux<MenuView> getUserMenuAsTree() {
return this
.getUserMenuAsList()
.as(MenuController::listToTree);
}
@GetMapping("/user-own/list")
@Authorize(merge = false)
@Operation(summary = "获取当前用户可访问的菜单(列表结构)")
public Flux<MenuView> getUserMenuAsList() {
return Authentication
.currentReactive()
.switchIfEmpty(Mono.error(UnAuthorizedException::new))
.flatMapMany(autz -> defaultMenuService
.createQuery()
.where(MenuEntity::getStatus,1)
.fetch()
.collect(Collectors.toMap(MenuEntity::getId, Function.identity()))
.flatMapIterable(menuMap -> MenuController
.convertMenuView(menuMap,
menu -> "admin".equals(autz.getUser().getUsername()) ||
menu.hasPermission(autz::hasPermission),
button -> "admin".equals(autz.getUser().getUsername()) ||
button.hasPermission(autz::hasPermission)
)));
}
@PutMapping("/_grant")
@Operation(summary = "根据菜单进行授权")
@ResourceAction(id = "grant", name = "授权")
public Mono<Void> grant(@RequestBody Mono<MenuGrantRequest> body) {
return Mono
.zip(
//T1: 当前用户权限信息
Authentication.currentReactive(),
//T2: 将菜单信息转为授权信息
Mono
.zip(body,
defaultMenuService
.createQuery()
.where(MenuEntity::getStatus,1)
.fetch()
.collectList(),
MenuGrantRequest::toAuthorizationSettingDetail
)
.map(Flux::just),
//保存授权信息
settingService::saveDetail
)
.flatMap(Function.identity());
}
@GetMapping("/{targetType}/{targetId}/_grant/tree")
@ResourceAction(id = "grant", name = "授权")
@Operation(summary = "获取菜单授权信息(树结构)")
public Flux<MenuView> getGrantInfoTree(@PathVariable String targetType,
@PathVariable String targetId) {
return this
.getGrantInfo(targetType, targetId)
.as(MenuController::listToTree);
}
@GetMapping("/{targetType}/{targetId}/_grant/list")
@ResourceAction(id = "grant", name = "授权")
@Operation(summary = "获取菜单授权信息(列表结构)")
public Flux<MenuView> getGrantInfo(@PathVariable String targetType,
@PathVariable String targetId) {
return Mono
.zip(
//权限设置信息
settingService.getSettingDetail(targetType, targetId),
//菜单
defaultMenuService
.createQuery()
.where(MenuEntity::getStatus,1)
.fetch()
.collectMap(MenuEntity::getId, Function.identity()),
(detail, menuMap) -> MenuController
.convertMenuView(menuMap,
menu -> menu.hasPermission(detail::hasPermission),
button -> button.hasPermission(detail::hasPermission)
)
)
.flatMapIterable(Function.identity());
}
private static Flux<MenuView> listToTree(Flux<MenuView> flux) {
return flux
.collectList()
.flatMapIterable(list -> TreeSupportEntity
.list2tree(list,
MenuView::setChildren,
(Predicate<MenuView>) n ->
StringUtils.isEmpty(n.getParentId())
|| "-1".equals(n.getParentId())));
}
private static Collection<MenuView> convertMenuView(Map<String, MenuEntity> menuMap,
Predicate<MenuEntity> menuPredicate,
Predicate<MenuButtonInfo> buttonPredicate) {
Map<String, MenuEntity> group = new HashMap<>();
for (MenuEntity menu : menuMap.values()) {
if (group.containsKey(menu.getId())) {
continue;
}
if (autz.getUser().getUsername().equals("admin") || AuthenticationUtils.createPredicate(menu.getPermissionExpression()).test(autz)) {
if (menuPredicate.test(menu)) {
String parentId = menu.getParentId();
MenuEntity parent;
group.put(menu.getId(), menu);
//有子菜单默认就有父菜单
while (!StringUtils.isEmpty(parentId)) {
parent = menuMap.get(parentId);
if (parent == null) {
@ -64,33 +183,12 @@ public class MenuController implements ReactiveServiceCrudController<MenuEntity,
}
}
}
List<MenuEntity> list = new ArrayList<>(group.values());
Collections.sort(list);
return list;
return group
.values()
.stream()
.map(menu -> MenuView.of(menu.copy(buttonPredicate)))
.sorted()
.collect(Collectors.toList());
}
/**
* 获取用户自己的菜单列表
* @return 菜单列表
*/
@GetMapping("user-own/tree")
@Authorize(merge = false)
public Flux<MenuEntity> getUserMenuAsTree() {
return Authentication
.currentReactive()
.switchIfEmpty(Mono.error(UnAuthorizedException::new))
.flatMapMany(autz -> defaultMenuService
.createQuery()
.fetch()
.collect(Collectors.toMap(MenuEntity::getId, Function.identity()))
.map(menuMap -> predicateUserMenu(menuMap, autz))
.map(menus -> TreeSupportEntity.list2tree(
menus,
MenuEntity::setChildren,
(Predicate<MenuEntity>) n ->
StringUtils.isEmpty(n.getParentId())
|| "-1".equals(n.getParentId()))).flatMapMany(Flux::fromIterable));
}
}

View File

@ -1,5 +1,7 @@
package org.jetlinks.community.auth.web.request;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import org.apache.commons.collections.CollectionUtils;
import org.hswebframework.web.authorization.Dimension;
@ -25,56 +27,85 @@ public class AuthorizationSettingDetail {
* 设置目标类型(维度)标识,: org, role
*/
@NotBlank
@Schema(description = "权限类型,如: org,openApi")
private String targetType;
/**
* 设置目标.
*/
@NotBlank
@Schema(description = "权限类型对应的数据ID")
private String targetId;
/**
* 冲突时是否合并
*/
@Schema(description = "冲突时是否合并")
private boolean merge = true;
/**
* 冲突时优先级
*/
@Schema(description = "冲突时合并优先级")
private int priority = 10;
/**
* 权限列表
*/
@Schema(description = "权限列表")
private List<PermissionInfo> permissionList;
public boolean hasPermission(String id, Collection<String> actions) {
if (CollectionUtils.isEmpty(permissionList)) {
return false;
}
for (PermissionInfo info : permissionList) {
if (Objects.equals(info.getId(), id)) {
if (CollectionUtils.isEmpty(actions)) {
return true;
}
if (CollectionUtils.isNotEmpty(info.getActions())) {
if (info.getActions().containsAll(actions)) {
return true;
}
}
}
}
return false;
}
/**
* 授权信息
*/
@Getter
@Setter
@EqualsAndHashCode(of = "id")
@Generated
public static class PermissionInfo {
/**
* 权限ID
*/
@NotBlank
@Schema(description = "权限ID")
private String id;
/**
* 授权操作
*/
@Schema(description = "允许执行的操作")
private Set<String> actions;
/**
* 字段权限
*/
@Hidden
private List<FieldAccess> fieldAccess;
/**
* 数据权限
*/
@Hidden
private List<DataAccess> dataAccess;
private PermissionInfo unwrap(AuthorizationSettingEntity entity) {
@ -94,8 +125,8 @@ public class AuthorizationSettingDetail {
//字段权限
if (DataAccessConfig.DefaultType.DENY_FIELDS.equalsIgnoreCase(access.getType())) {
Set<String> fields = Optional.ofNullable(access.getConfig())
.<Set<String>>map(conf -> new HashSet<>((Collection<String>) conf.get("fields")))
.orElseGet(HashSet::new);
.<Set<String>>map(conf -> new HashSet<>((Collection<String>) conf.get("fields")))
.orElseGet(HashSet::new);
for (String field : fields) {
filedAccessMap
@ -130,7 +161,7 @@ public class AuthorizationSettingDetail {
for (FieldAccess access : fieldAccess) {
for (String action : access.getAction()) {
group.computeIfAbsent(action, r -> new HashSet<>())
.add(access.name);
.add(access.name);
}
}
for (Map.Entry<String, Set<String>> entry : group.entrySet()) {
@ -150,6 +181,12 @@ public class AuthorizationSettingDetail {
entity.setDataAccesses(entities);
}
public static PermissionInfo of(String id, Collection<String> actions) {
PermissionInfo info = new PermissionInfo();
info.setId(id);
info.setActions(new HashSet<>(actions));
return info;
}
}
@ -184,7 +221,7 @@ public class AuthorizationSettingDetail {
}
public List<DataAccessEntity> toEntity() {
if(CollectionUtils.isEmpty(actions)){
if (CollectionUtils.isEmpty(actions)) {
return Collections.emptyList();
}
return actions

View File

@ -0,0 +1,97 @@
package org.jetlinks.community.auth.web.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.commons.collections4.CollectionUtils;
import org.hswebframework.web.api.crud.entity.TreeSupportEntity;
import org.hswebframework.web.id.IDGenerator;
import org.jetlinks.community.auth.entity.MenuView;
import org.jetlinks.community.auth.entity.MenuEntity;
import org.jetlinks.community.auth.entity.MenuView;
import org.jetlinks.community.auth.entity.PermissionInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class MenuGrantRequest {
private String targetType;
private String targetId;
/**
* 冲突时是否合并
*/
@Schema(description = "冲突时是否合并")
private boolean merge = true;
/**
* 冲突时优先级
*/
@Schema(description = "冲突时合并优先级")
private int priority = 10;
@Schema(description = "授权的菜单信息")
private List<MenuView> menus;
public AuthorizationSettingDetail toAuthorizationSettingDetail(List<MenuEntity> menuEntities) {
Map<String, MenuEntity> menuMap = menuEntities
.stream()
.collect(Collectors.toMap(MenuEntity::getId, Function.identity()));
AuthorizationSettingDetail detail = new AuthorizationSettingDetail();
detail.setTargetType(targetType);
detail.setTargetId(targetId);
detail.setMerge(merge);
detail.setPriority(priority);
List<AuthorizationSettingDetail.PermissionInfo> permissionInfos = new ArrayList<>();
for (MenuView menu : menus) {
//平铺
List<MenuView> expand = TreeSupportEntity.expandTree2List(menu, IDGenerator.MD5);
for (MenuView menuView : expand) {
MenuEntity entity = menuMap.get(menuView.getId());
if (entity == null) {
continue;
}
//自动持有配置的权限
if (CollectionUtils.isNotEmpty(entity.getPermissions())) {
for (PermissionInfo permission : entity.getPermissions()) {
permissionInfos.add(AuthorizationSettingDetail.PermissionInfo.of(permission.getPermission(), permission.getActions()));
}
}
if (CollectionUtils.isNotEmpty(menuView.getButtons())) {
for (MenuView.ButtonView button : menuView.getButtons()) {
entity.getButton(button.getId())
.ifPresent(buttonInfo -> {
if (CollectionUtils.isNotEmpty(buttonInfo.getPermissions())) {
for (PermissionInfo permission : buttonInfo.getPermissions()) {
permissionInfos
.add(AuthorizationSettingDetail.PermissionInfo.of(permission.getPermission(), permission.getActions()));
}
}
});
}
}
}
}
detail.setPermissionList(permissionInfos);
return detail;
}
}