refactor: 优化权限控制

This commit is contained in:
zhouhao
2025-08-05 09:33:28 +08:00
parent 3f7e040c44
commit 4dda9eb683
16 changed files with 333 additions and 135 deletions

View File

@@ -17,6 +17,7 @@
package org.hswebframework.web.authorization;
import org.hswebframework.web.authorization.annotation.Logical;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
@@ -93,18 +94,37 @@ public interface Authentication extends Serializable {
* @return 用户持有的权限集合
*/
List<Permission> getPermissions();
default boolean hasDimension(String type, String... id) {
return hasDimension(type, Arrays.asList(id));
return hasAnyDimension(type, Arrays.asList(id));
}
default boolean hasAllDimension(String type, Collection<String> id) {
if (id.isEmpty()) {
return !getDimensions(type).isEmpty();
}
return getDimensions(type)
.stream()
.allMatch(p -> id.contains(p.getId()));
}
default boolean hasAnyDimension(String type, Collection<String> id) {
if (id.isEmpty()) {
return !getDimensions(type).isEmpty();
}
return getDimensions(type)
.stream()
.anyMatch(p -> id.contains(p.getId()));
}
@Deprecated
default boolean hasDimension(String type, Collection<String> id) {
if (id.isEmpty()) {
return !getDimensions(type).isEmpty();
}
return getDimensions(type)
.stream()
.anyMatch(p -> id.contains(p.getId()));
.stream()
.anyMatch(p -> id.contains(p.getId()));
}
default boolean hasDimension(DimensionType type, String id) {
@@ -116,9 +136,9 @@ public interface Authentication extends Serializable {
return Optional.empty();
}
return getDimensions()
.stream()
.filter(dimension -> dimension.getId().equals(id) && type.equalsIgnoreCase(dimension.getType().getId()))
.findFirst();
.stream()
.filter(dimension -> dimension.getId().equals(id) && type.equalsIgnoreCase(dimension.getType().getId()))
.findFirst();
}
default Optional<Dimension> getDimension(DimensionType type, String id) {
@@ -126,9 +146,9 @@ public interface Authentication extends Serializable {
return Optional.empty();
}
return getDimensions()
.stream()
.filter(dimension -> dimension.getId().equals(id) && type.isSameType(dimension.getType()))
.findFirst();
.stream()
.filter(dimension -> dimension.getId().equals(id) && type.isSameType(dimension.getType()))
.findFirst();
}
@@ -137,9 +157,9 @@ public interface Authentication extends Serializable {
return Collections.emptyList();
}
return getDimensions()
.stream()
.filter(dimension -> dimension.getType().isSameType(type))
.collect(Collectors.toList());
.stream()
.filter(dimension -> dimension.getType().isSameType(type))
.collect(Collectors.toList());
}
default List<Dimension> getDimensions(DimensionType type) {
@@ -147,9 +167,9 @@ public interface Authentication extends Serializable {
return Collections.emptyList();
}
return getDimensions()
.stream()
.filter(dimension -> dimension.getType().isSameType(type))
.collect(Collectors.toList());
.stream()
.filter(dimension -> dimension.getType().isSameType(type))
.collect(Collectors.toList());
}
@@ -164,9 +184,9 @@ public interface Authentication extends Serializable {
return Optional.empty();
}
return getPermissions()
.stream()
.filter(permission -> permission.getId().equals(id))
.findAny();
.stream()
.filter(permission -> permission.getId().equals(id))
.findAny();
}
/**
@@ -179,8 +199,8 @@ public interface Authentication extends Serializable {
default boolean hasPermission(String permissionId, String... actions) {
return hasPermission(permissionId,
actions.length == 0
? Collections.emptyList()
: Arrays.asList(actions));
? Collections.emptyList()
: Arrays.asList(actions));
}
default boolean hasPermission(String permissionId, Collection<String> actions) {
@@ -190,8 +210,8 @@ public interface Authentication extends Serializable {
}
if (Objects.equals(permissionId, permission.getId())) {
return actions.isEmpty()
|| permission.getActions().containsAll(actions)
|| permission.getActions().contains("*");
|| permission.getActions().containsAll(actions)
|| permission.getActions().contains("*");
}
}
return false;

View File

@@ -18,7 +18,7 @@ public interface AuthenticationPredicate extends Predicate<Authentication> {
}
static AuthenticationPredicate dimension(String dimension, String... id) {
return autz -> autz.hasDimension(dimension, Arrays.asList(id));
return autz -> autz.hasAnyDimension(dimension, Arrays.asList(id));
}
static AuthenticationPredicate permission(String permissionId, String... actions) {

View File

@@ -1,20 +1,67 @@
package org.hswebframework.web.authorization.annotation;
import org.hswebframework.web.authorization.DimensionType;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 请使用注解继承方式使用此注解
*
* @author zhouhao
* @see RequiresRoles
* @since 4.0
*/
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Repeatable(value = Dimension.List.class)
public @interface Dimension {
/**
* 维度类型标识,如: role,org
*
* @return 维度类型
* @see org.hswebframework.web.authorization.Dimension#getType()
* @see DimensionType#getId()
* @see org.hswebframework.web.authorization.Authentication#hasDimension(String, String...)
*/
String type();
/**
* 具体的维度ID,如: 角色ID,组织ID
*
* @return 维度ID
* @see org.hswebframework.web.authorization.Dimension#getId()
* @see org.hswebframework.web.authorization.Authentication#hasDimension(String, String...)
*/
String[] id() default {};
/**
* 配置了多个ID时的判断逻辑,默认为任意满足则认为有权限.
*
* @return Logical
*/
Logical logical() default Logical.DEFAULT;
/**
* @return 说明
*/
String[] description() default {};
/**
* @return 是否忽略
*/
boolean ignore() default false;
@Target({ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@Inherited
@interface List {
Dimension[] value() default {};
}
}

View File

@@ -0,0 +1,32 @@
package org.hswebframework.web.authorization.annotation;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 标记多个维度的权限控制相关配置
*
* @author zhouhao
* @since 5.0.1
*/
@Target({ElementType.METHOD, TYPE, ANNOTATION_TYPE, FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Dimensions {
/**
* 存在多个维度时的判断逻辑,默认任意一个满足则认为有权限
*
* @return Logical
*/
Logical logical() default Logical.DEFAULT;
/**
* @return 针对当前配置的说明信息
*/
String[] description() default {};
}

View File

@@ -5,17 +5,49 @@ import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 注解根据角色维度进行权限控制,具有权限的用户才可访问对应的方法.
*
* <pre>{@code
* @RequiresRoles("admin")
* public Mono<Void> handleRequest(){
*
* }
* }</pre>
*
* @author zhouhao
* @see Dimension
* @since 4.0
*/
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Dimension(type = "role", description = "控制角色")
@Dimension(type = "role")
@Repeatable(RequiresRoles.List.class)
public @interface RequiresRoles {
/**
* @return 角色ID
*/
@AliasFor(annotation = Dimension.class, attribute = "id")
String[] value() default {};
/**
* 多个角色时的判断逻辑
* @return Logical
*/
@AliasFor(annotation = Dimension.class, attribute = "logical")
Logical logical() default Logical.DEFAULT;
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.FIELD})
@Retention(RUNTIME)
@Documented
@Inherited
@Dimension.List()
@interface List {
RequiresRoles[] value();
}
}

View File

@@ -6,6 +6,9 @@ import org.hswebframework.web.authorization.define.Phased;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 接口资源声明注解,声明Controller的资源相关信息,用于进行权限控制。
* <br>
@@ -53,6 +56,7 @@ import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Repeatable(Resource.List.class)
public @interface Resource {
/**
@@ -98,4 +102,12 @@ public @interface Resource {
* @return 是否合并
*/
boolean merge() default true;
@Target({ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@Inherited
@interface List {
Resource[] value();
}
}

View File

@@ -6,9 +6,14 @@ import lombok.Setter;
import org.hswebframework.web.authorization.DimensionType;
import org.hswebframework.web.authorization.annotation.Logical;
import org.hswebframework.web.bean.FastBeanCopier;
import reactor.function.Predicate3;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
@Getter
@Setter
@@ -23,10 +28,27 @@ public class DimensionDefinition {
private Logical logical = Logical.DEFAULT;
public boolean hasDimension(Predicate3<String,Logical, Set<String>> filter) {
return filter.test(typeId,logical, Collections.unmodifiableSet(dimensionId));
}
public boolean hasDimension(Set<String> dimensionIdPredicate) {
if (logical == Logical.AND) {
return dimensionIdPredicate.containsAll(dimensionId);
}
return dimensionId
.stream()
.anyMatch(dimensionIdPredicate::contains);
}
public boolean hasDimension(String id) {
return dimensionId.contains(id);
}
public void addDimensionI(Set<String> id) {
dimensionId.addAll(id);
}
public DimensionDefinition copy() {
return FastBeanCopier.copy(this, DimensionDefinition::new);
}

View File

@@ -3,36 +3,64 @@ package org.hswebframework.web.authorization.define;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.collections4.Predicate;
import org.hswebframework.web.authorization.Dimension;
import org.hswebframework.web.authorization.annotation.Logical;
import reactor.function.Predicate3;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiPredicate;
import java.util.stream.Collectors;
@Getter
@Setter
public class DimensionsDefinition {
private Set<DimensionDefinition> dimensions = new HashSet<>();
private Map<String, DimensionDefinition> dimensionsMapping = new ConcurrentHashMap<>();
private Logical logical = Logical.DEFAULT;
public void addDimension(DimensionDefinition definition) {
dimensions.add(definition);
private String description;
public Set<DimensionDefinition> getDimensions() {
return new HashSet<>(dimensionsMapping.values());
}
public boolean isEmpty(){
return CollectionUtils.isEmpty(this.dimensions);
public void clear() {
dimensionsMapping.clear();
}
public void addDimension(DimensionDefinition definition) {
DimensionDefinition old = dimensionsMapping.putIfAbsent(definition.getTypeId(), definition);
if (old != null) {
old.addDimensionI(definition.getDimensionId());
}
}
public boolean isEmpty() {
return MapUtils.isEmpty(this.dimensionsMapping);
}
public boolean hasDimension(Dimension dimension) {
return dimensions
DimensionDefinition def = dimensionsMapping.get(dimension.getType().getId());
return def != null && def.hasDimension(dimension.getId());
}
public boolean hasDimension(Predicate3<String,Logical, Set<String>> filter) {
if (logical == Logical.AND) {
return dimensionsMapping
.values()
.stream()
.anyMatch(def ->
def.getTypeId().equals(dimension.getType().getId())
&& def.hasDimension(dimension.getId()));
.allMatch(e -> e.hasDimension(filter));
} else {
return dimensionsMapping
.values()
.stream()
.anyMatch(e -> e.hasDimension(filter));
}
}
public boolean hasDimension(List<Dimension> dimensions) {
@@ -43,4 +71,13 @@ public class DimensionsDefinition {
return dimensions.stream().anyMatch(this::hasDimension);
}
@Override
public String toString() {
return dimensionsMapping
.values()
.stream()
.map(d -> String.join(",", d.getDimensionId()) + "@" + d.getTypeId())
.collect(Collectors.joining(";"));
}
}

View File

@@ -80,11 +80,6 @@ public class ResourceDefinition implements MultipleI18nSupportEntity {
public synchronized ResourceDefinition addAction(ResourceActionDefinition action) {
actionIds = null;
ResourceActionDefinition old = getAction(action.getId()).orElse(null);
if (old != null) {
old.getDataAccess().getDataAccessTypes()
.addAll(action.getDataAccess().getDataAccessTypes());
}
actions.add(action);
return this;
}

View File

@@ -22,6 +22,9 @@ public class ResourcesDefinition {
private Phased phased = Phased.before;
public void clear(){
resources.clear();
}
public void addResource(ResourceDefinition resource, boolean merge) {
ResourceDefinition definition = getResource(resource.getId()).orElse(null);
if (definition != null) {

View File

@@ -111,9 +111,7 @@ public class AopAuthorizingController extends StaticMethodMatcherPointcutAdvisor
MethodInterceptorContext paramContext = holder.createParamContext();
AuthorizeDefinition definition = aopMethodAuthorizeDefinitionParser
.parse(methodInvocation
.getThis()
.getClass(),
.parse(methodInvocation.getThis().getClass(),
methodInvocation.getMethod(),
paramContext);
Object result = null;
@@ -131,43 +129,26 @@ public class AopAuthorizingController extends StaticMethodMatcherPointcutAdvisor
Authentication authentication = Authentication
.current()
.orElseThrow(UnAuthorizedException.NoStackTrace::new);
.orElse(null);
if (authentication == null) {
// 允许匿名访问
if (definition.allowAnonymous()) {
return methodInvocation.proceed();
}
return new UnAuthorizedException.NoStackTrace();
}
context.setAuthentication(authentication);
isControl = true;
Phased dataAccessPhased = definition.getResources().getPhased();
if (definition.getPhased() == Phased.before) {
//RDAC before
authorizingHandler.handRBAC(context);
//方法调用前验证数据权限
if (dataAccessPhased == Phased.before) {
authorizingHandler.handleDataAccess(context);
}
result = methodInvocation.proceed();
//方法调用后验证数据权限
if (dataAccessPhased == Phased.after) {
context.setParamContext(holder.createParamContext(result));
authorizingHandler.handleDataAccess(context);
}
} else {
//方法调用前验证数据权限
if (dataAccessPhased == Phased.before) {
authorizingHandler.handleDataAccess(context);
}
result = methodInvocation.proceed();
context.setParamContext(holder.createParamContext(result));
authorizingHandler.handRBAC(context);
//方法调用后验证数据权限
if (dataAccessPhased == Phased.after) {
authorizingHandler.handleDataAccess(context);
}
}
}
if (!isControl) {

View File

@@ -92,7 +92,7 @@ public class DefaultAopMethodAuthorizeDefinitionParser implements AopMethodAutho
}
}
public CacheKey buildCacheKey(Class<?> target, Method method) {
CacheKey buildCacheKey(Class<?> target, Method method) {
return new CacheKey(ClassUtils.getUserClass(target), method);
}

View File

@@ -8,6 +8,8 @@ import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.CollectionUtils;
import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Function;
@@ -20,7 +22,8 @@ public class AopAuthorizeDefinitionParser {
Authorize.class,
Dimension.class,
Resource.class,
ResourceAction.class
ResourceAction.class,
Dimensions.class
));
private final Set<Annotation> methodAnnotation;
@@ -37,10 +40,8 @@ public class AopAuthorizeDefinitionParser {
definition = new DefaultBasicAuthorizeDefinition();
definition.setTargetClass(targetClass);
definition.setTargetMethod(method);
methodAnnotation = AnnotatedElementUtils.findAllMergedAnnotations(method, types);
classAnnotation = AnnotatedElementUtils.findAllMergedAnnotations(targetClass, types);
methodAnnotation = loadAnnotations(method);
classAnnotation = loadAnnotations(targetClass);
classAnnotationGroup = classAnnotation
.stream()
@@ -51,6 +52,23 @@ public class AopAuthorizeDefinitionParser {
.collect(Collectors.groupingBy(Annotation::annotationType));
}
private Set<Annotation> loadAnnotations(AnnotatedElement element) {
return types
.stream()
.flatMap(s -> {
if (s.isAnnotationPresent(Repeatable.class)) {
return AnnotatedElementUtils
.findMergedRepeatableAnnotations(element, s)
.stream();
}
return AnnotatedElementUtils
.findAllMergedAnnotations(element, s)
.stream();
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
private void initClassAnnotation() {
for (Annotation annotation : classAnnotation) {
if (annotation instanceof Authorize) {
@@ -73,12 +91,16 @@ public class AopAuthorizeDefinitionParser {
if (annotation instanceof Dimension) {
definition.putAnnotation(((Dimension) annotation));
}
if (annotation instanceof Dimensions) {
definition.putAnnotation(((Dimensions) annotation));
}
if (annotation instanceof ResourceAction) {
getAnnotationByType(Resource.class)
.map(res -> definition.getResources().getResource(res.id()).orElse(null))
.filter(Objects::nonNull)
.forEach(res -> {
definition.putAnnotation(res, (ResourceAction) annotation);
definition.putAnnotation(res, (ResourceAction) annotation);
});
}
}

View File

@@ -61,10 +61,17 @@ public class DefaultBasicAuthorizeDefinition implements AopAuthorizeDefinition {
return parser.parse();
}
public void putAnnotation(Dimensions ann) {
dimensions.setLogical(ann.logical());
if (ann.description().length > 0) {
dimensions.setDescription(String.join("", ann.description()));
}
}
public void putAnnotation(Authorize ann) {
if (!ann.merge()) {
getResources().getResources().clear();
getDimensions().getDimensions().clear();
getResources().clear();
getDimensions().clear();
}
setPhased(ann.phased());
getResources().setPhased(ann.phased());
@@ -81,7 +88,7 @@ public class DefaultBasicAuthorizeDefinition implements AopAuthorizeDefinition {
public void putAnnotation(Dimension ann) {
if (ann.ignore()) {
getDimensions().getDimensions().clear();
getDimensions().clear();
return;
}
DimensionDefinition definition = new DimensionDefinition();

View File

@@ -5,10 +5,8 @@ import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.authorization.Permission;
import org.hswebframework.web.authorization.access.DataAccessController;
import org.hswebframework.web.authorization.define.AuthorizeDefinition;
import org.hswebframework.web.authorization.define.AuthorizingContext;
import org.hswebframework.web.authorization.define.HandleType;
import org.hswebframework.web.authorization.define.ResourcesDefinition;
import org.hswebframework.web.authorization.annotation.Logical;
import org.hswebframework.web.authorization.define.*;
import org.hswebframework.web.authorization.events.AuthorizingHandleBeforeEvent;
import org.hswebframework.web.authorization.exception.AccessDenyException;
import org.slf4j.Logger;
@@ -17,6 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import reactor.core.publisher.Mono;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
@@ -93,10 +92,7 @@ public class DefaultAuthorizingHandler implements AuthorizingHandler {
AuthorizingHandleBeforeEvent event = new AuthorizingHandleBeforeEvent(context, type);
eventPublisher.publishEvent(event);
if (event.hasListener()) {
event
.getAsync()
.toFuture()
.get(10, TimeUnit.SECONDS);
event.getAsync().block();
}
if (!event.isExecute()) {
if (event.isAllow()) {
@@ -109,51 +105,39 @@ public class DefaultAuthorizingHandler implements AuthorizingHandler {
return false;
}
@Deprecated
public void handleDataAccess(AuthorizingContext context) {
if (dataAccessController == null) {
return;
}
if (context.getDefinition().getResources() == null) {
return;
}
if (handleEvent(context, HandleType.DATA)) {
return;
}
DataAccessController finalAccessController = dataAccessController;
Authentication autz = context.getAuthentication();
boolean isAccess = context
.getDefinition()
.getResources()
.getDataAccessResources()
.stream()
.allMatch(resource -> {
Permission permission = autz
.getPermission(resource.getId())
.orElseThrow(AccessDenyException.NoStackTrace::new);
return resource
.getDataAccessAction()
.stream()
.allMatch(act -> permission
.getDataAccesses(act.getId())
.stream()
.allMatch(dataAccessConfig -> finalAccessController.doAccess(dataAccessConfig, context)));
});
if (!isAccess) {
throw new AccessDenyException.NoStackTrace(context.getDefinition().getMessage());
}
}
protected void handleRBAC(Authentication authentication, AuthorizeDefinition definition) {
ResourcesDefinition resources = definition.getResources();
// 判断权限
if (!resources.hasPermission(authentication)) {
throw new AccessDenyException.NoStackTrace(definition.getMessage(), definition.getDescription());
}
DimensionsDefinition dd = definition.getDimensions();
// 判断维度
if (dd != null && !dd.isEmpty()) {
if (!dd.hasDimension(
(type, logical, dimensionIds) ->
hasDimensions(authentication, type, logical, dimensionIds))) {
throw new AccessDenyException
.NoStackTrace(definition.getMessage(), definition.getDimensions().toString());
}
}
}
private boolean hasDimensions(Authentication auth, String type, Logical logical, Set<String> dimensionIds) {
if (logical == Logical.AND) {
return auth.hasAllDimension(type, dimensionIds);
}
return auth.hasAnyDimension(type, dimensionIds);
}
}

View File

@@ -16,25 +16,27 @@ public class DefaultBasicAuthorizeDefinitionTest {
@SneakyThrows
public void testCustomAnn() {
AopAuthorizeDefinition definition =
DefaultBasicAuthorizeDefinition.from(TestController.class, TestController.class.getMethod("test"));
DefaultBasicAuthorizeDefinition.from(TestController.class, TestController.class.getMethod("test"));
ResourceDefinition resource = definition.getResources()
.getResource("test").orElseThrow(NullPointerException::new);
ResourceDefinition resource = definition
.getResources()
.getResource("test").orElseThrow(NullPointerException::new);
Assert.assertNotNull(resource);
Assert.assertTrue(resource.hasAction(Arrays.asList("add")));
System.out.println(definition.getDimensions());
Assert.assertFalse(definition.getDimensions().isEmpty());
Assert.assertEquals(1, definition.getDimensions().getDimensions().size());
Assert.assertTrue(resource.getAction("add")
.map(act->act.getDataAccess().getType("user_own_data"))
.isPresent());
}
@Test
@SneakyThrows
public void testNoMerge() {
AopAuthorizeDefinition definition =
DefaultBasicAuthorizeDefinition.from(TestController.class, TestController.class.getMethod("noMerge"));
DefaultBasicAuthorizeDefinition.from(TestController.class, TestController.class.getMethod("noMerge"));
Assert.assertTrue(definition.getResources().isEmpty());
}
@@ -43,17 +45,19 @@ public class DefaultBasicAuthorizeDefinitionTest {
public class TestController implements GenericController {
@Authorize(merge = false)
public void noMerge(){
public void noMerge() {
}
}
public interface GenericController {
@CreateAction
@UserOwnData
default void test(){
@RequiresRoles("test")
@RequiresRoles("test2")
default void test() {
}
}