mirror of
https://gitee.com/pnoker/iot-dc3.git
synced 2026-11-03 03:32:08 +08:00
fix(auth): scope menus and permission registration
This commit is contained in:
+38
-3
@@ -20,11 +20,13 @@ package io.github.pnoker.common.auth.controller;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.github.pnoker.common.auth.entity.bo.MenuBO;
|
||||
import io.github.pnoker.common.auth.entity.bo.MenuTreeBO;
|
||||
import io.github.pnoker.common.auth.entity.bo.ResourceBO;
|
||||
import io.github.pnoker.common.auth.entity.builder.MenuBuilder;
|
||||
import io.github.pnoker.common.auth.entity.query.MenuQuery;
|
||||
import io.github.pnoker.common.auth.entity.vo.MenuTreeVO;
|
||||
import io.github.pnoker.common.auth.entity.vo.MenuVO;
|
||||
import io.github.pnoker.common.auth.service.MenuService;
|
||||
import io.github.pnoker.common.auth.service.RoleResourceBindService;
|
||||
import io.github.pnoker.common.base.BaseController;
|
||||
import io.github.pnoker.common.constant.service.AuthConstant;
|
||||
import io.github.pnoker.common.entity.R;
|
||||
@@ -47,6 +49,8 @@ import reactor.core.publisher.Mono;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -69,6 +73,8 @@ public class MenuController implements BaseController {
|
||||
|
||||
private final MenuService menuService;
|
||||
|
||||
private final RoleResourceBindService roleResourceBindService;
|
||||
|
||||
@PreAuthorize("@perm.can('menu', 'add')")
|
||||
@Operation(summary = "新增菜单管理", description = "新增一条菜单记录")
|
||||
@PostMapping("/add")
|
||||
@@ -139,15 +145,44 @@ public class MenuController implements BaseController {
|
||||
@Operation(summary = "查询菜单管理列表", description = "分页查询菜单管理列表")
|
||||
@PostMapping("/list_tree")
|
||||
public Mono<R<List<MenuTreeVO>>> listTree(@RequestBody(required = false) MenuQuery entityQuery) {
|
||||
// Read access to global menu data is open to all authenticated users.
|
||||
return async(() -> {
|
||||
return getUserHeader().flatMap(header -> async(() -> {
|
||||
List<MenuTreeBO> entityBOList = menuService.listTree(entityQuery);
|
||||
entityBOList = filterByUserMenuResources(entityBOList, header.getUserId(), header.getTenantId());
|
||||
List<MenuTreeVO> entityVOList = new ArrayList<>(entityBOList.size());
|
||||
for (MenuTreeBO node : entityBOList) {
|
||||
entityVOList.add(toTreeVO(node));
|
||||
}
|
||||
return R.ok(entityVOList);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private List<MenuTreeBO> filterByUserMenuResources(List<MenuTreeBO> nodes, Long userId, Long tenantId) {
|
||||
List<ResourceBO> resources = roleResourceBindService.listResourceByUserId(userId, tenantId);
|
||||
Set<String> visibleMenuCodes = resources.stream()
|
||||
.map(ResourceBO::getResourceCode)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
if (visibleMenuCodes.contains("*")) {
|
||||
return nodes;
|
||||
}
|
||||
return nodes.stream()
|
||||
.map(node -> retainAccessibleMenuNode(node, visibleMenuCodes))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private MenuTreeBO retainAccessibleMenuNode(MenuTreeBO node, Set<String> visibleMenuCodes) {
|
||||
List<MenuTreeBO> children = Objects.requireNonNullElse(node.getChildren(), List.<MenuTreeBO>of())
|
||||
.stream()
|
||||
.map(child -> retainAccessibleMenuNode(child, visibleMenuCodes))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
boolean selfVisible = visibleMenuCodes.contains("menu:" + Objects.requireNonNullElse(node.getMenuCode(), ""));
|
||||
if (!selfVisible && children.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
node.setChildren(children);
|
||||
return node;
|
||||
}
|
||||
|
||||
private MenuTreeVO toTreeVO(MenuTreeBO node) {
|
||||
|
||||
+27
@@ -21,6 +21,7 @@ import io.github.pnoker.common.constant.common.SymbolConstant;
|
||||
import io.github.pnoker.common.facade.entity.bo.FacadeScannedApiBO;
|
||||
import io.github.pnoker.common.resource.registrar.config.ResourceRegistrarProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
@@ -34,6 +35,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Walks the WebFlux handler mappings and turns every HTTP endpoint into a
|
||||
@@ -53,6 +56,9 @@ public class ApiEndpointScanner {
|
||||
|
||||
private static final List<String> DEFAULT_EXCLUDES = List.of("/actuator/**", "/error", "/error/**", "/favicon.ico");
|
||||
|
||||
private static final Pattern PERMISSION_CAN_PATTERN = Pattern.compile(
|
||||
"@perm\\.can\\(\\s*['\"]([^'\"]+)['\"]\\s*,\\s*['\"]([^'\"]+)['\"]\\s*\\)");
|
||||
|
||||
private final RequestMappingHandlerMapping handlerMapping;
|
||||
|
||||
private final ResourceRegistrarProperties properties;
|
||||
@@ -68,6 +74,11 @@ public class ApiEndpointScanner {
|
||||
*/
|
||||
|
||||
private static String buildApiName(HandlerMethod handler, String httpMethod, boolean isSingleGet) {
|
||||
String protectedApiName = resolvePreAuthorizeApiName(handler);
|
||||
if (Objects.nonNull(protectedApiName)) {
|
||||
return protectedApiName;
|
||||
}
|
||||
|
||||
String className = handler.getBeanType().getSimpleName();
|
||||
// Strip "Controller" suffix, convert camelCase to snake_case → domain
|
||||
String domain = className
|
||||
@@ -89,6 +100,22 @@ public class ApiEndpointScanner {
|
||||
return domain + SymbolConstant.COLON + scope;
|
||||
}
|
||||
|
||||
private static String resolvePreAuthorizeApiName(HandlerMethod handler) {
|
||||
PreAuthorize preAuthorize = handler.getMethodAnnotation(PreAuthorize.class);
|
||||
if (Objects.isNull(preAuthorize)) {
|
||||
preAuthorize = handler.getBeanType().getAnnotation(PreAuthorize.class);
|
||||
}
|
||||
if (Objects.isNull(preAuthorize)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Matcher matcher = PERMISSION_CAN_PATTERN.matcher(preAuthorize.value());
|
||||
if (!matcher.find()) {
|
||||
return null;
|
||||
}
|
||||
return matcher.group(1) + SymbolConstant.COLON + matcher.group(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan all registered WebFlux request mappings and return the endpoint inventory
|
||||
* visible to the permission resource registrar.
|
||||
|
||||
+45
@@ -21,6 +21,7 @@ import io.github.pnoker.common.facade.entity.bo.FacadeScannedApiBO;
|
||||
import io.github.pnoker.common.resource.registrar.config.ResourceRegistrarProperties;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
@@ -122,6 +123,22 @@ class ApiEndpointScannerTest {
|
||||
assertThat(scanner.scan()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void preAuthorizePermissionIsUsedAsApiNameWhenPresent() {
|
||||
register(ProjectConventionController.class);
|
||||
ApiEndpointScanner scanner = new ApiEndpointScanner(handlerMapping, new ResourceRegistrarProperties());
|
||||
|
||||
List<FacadeScannedApiBO> apis = scanner.scan();
|
||||
|
||||
assertThat(apis).extracting(FacadeScannedApiBO::getMethod, FacadeScannedApiBO::getPath,
|
||||
FacadeScannedApiBO::getApiName)
|
||||
.containsExactlyInAnyOrder(
|
||||
org.assertj.core.groups.Tuple.tuple("GET", "/api/convention/get_by_id", "convention:get"),
|
||||
org.assertj.core.groups.Tuple.tuple("POST", "/api/convention/list", "convention:list"),
|
||||
org.assertj.core.groups.Tuple.tuple("POST", "/api/convention/update", "convention:update"),
|
||||
org.assertj.core.groups.Tuple.tuple("POST", "/api/convention/delete", "convention:delete"));
|
||||
}
|
||||
|
||||
private void register(Class<?> controllerClass) {
|
||||
try {
|
||||
Object bean = controllerClass.getDeclaredConstructor().newInstance();
|
||||
@@ -203,4 +220,32 @@ class ApiEndpointScannerTest {
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/convention")
|
||||
static class ProjectConventionController {
|
||||
@PreAuthorize("@perm.can('convention', 'get')")
|
||||
@GetMapping("/get_by_id")
|
||||
public String getById() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@PreAuthorize("@perm.can('convention', 'list')")
|
||||
@PostMapping("/list")
|
||||
public String list() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@PreAuthorize("@perm.can('convention', 'update')")
|
||||
@PostMapping("/update")
|
||||
public String update() {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@PreAuthorize("@perm.can('convention', 'delete')")
|
||||
@PostMapping("/delete")
|
||||
public String delete() {
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user