refactor: 优化权限合并逻辑

This commit is contained in:
zhouhao
2026-01-06 14:38:36 +08:00
parent f87356ccbb
commit 2eb26c4b38
10 changed files with 1067 additions and 92 deletions

View File

@@ -204,10 +204,8 @@ public interface Authentication extends Serializable {
default boolean hasPermission(String permissionId, Collection<String> actions) {
for (Permission permission : getPermissions()) {
if (Objects.equals(permission.getId(), "*")) {
return true;
}
if (Objects.equals(permissionId, permission.getId())) {
if (Objects.equals(permission.getId(), "*") ||
Objects.equals(permissionId, permission.getId())) {
return actions.isEmpty()
|| permission.getActions().containsAll(actions)
|| permission.getActions().contains("*");

View File

@@ -65,8 +65,8 @@ public final class AuthenticationHolder {
if (size == 1) {
return function.apply(suppliers.get(0));
}
ReactiveAuthenticationHolder.AuthenticationMerging merging
= new ReactiveAuthenticationHolder.AuthenticationMerging();
AuthenticationUtils.AuthenticationMerging merging
= new AuthenticationUtils.AuthenticationMerging();
for (AuthenticationSupplier supplier : suppliers) {
function.apply(supplier).ifPresent(merging::merge);
}

View File

@@ -1,7 +1,10 @@
package org.hswebframework.web.authorization;
import org.hswebframework.web.authorization.simple.SimpleAuthentication;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author zhouhao
@@ -9,6 +12,37 @@ import org.springframework.util.StringUtils;
*/
public class AuthenticationUtils {
public static Mono<Authentication> merge(Flux<Authentication> authenticationFlux){
return authenticationFlux
.collect(AuthenticationMerging::new, AuthenticationMerging::merge)
.mapNotNull(AuthenticationMerging::get);
}
static class AuthenticationMerging {
private Authentication auth;
private int count;
public synchronized void merge(Authentication auth) {
if (this.auth == null || this.auth == auth) {
this.auth = auth;
} else {
if (count++ == 0) {
SimpleAuthentication newAuth = new SimpleAuthentication();
newAuth.merge(this.auth);
this.auth = newAuth;
}
this.auth.merge(auth);
}
}
Authentication get() {
return auth;
}
}
public static AuthenticationPredicate createPredicate(String expression) {
if (ObjectUtils.isEmpty(expression)) {
return (authentication -> false);

View File

@@ -46,10 +46,9 @@ public final class ReactiveAuthenticationHolder {
private static final List<ReactiveAuthenticationSupplier> suppliers = new CopyOnWriteArrayList<>();
private static Mono<Authentication> get(Function<ReactiveAuthenticationSupplier, Mono<Authentication>> function) {
return Flux
.merge(Lists.transform(suppliers, function::apply))
.collect(AuthenticationMerging::new, AuthenticationMerging::merge)
.mapNotNull(AuthenticationMerging::get);
return AuthenticationUtils
.merge(Flux.merge(Lists.transform(suppliers, function::apply)))
;
}
/**
@@ -85,27 +84,5 @@ public final class ReactiveAuthenticationHolder {
}
static class AuthenticationMerging {
private Authentication auth;
private int count;
public synchronized void merge(Authentication auth) {
if (this.auth == null || this.auth == auth) {
this.auth = auth;
} else {
if (count++ == 0) {
SimpleAuthentication newAuth = new SimpleAuthentication();
newAuth.merge(this.auth);
this.auth = newAuth;
}
this.auth.merge(auth);
}
}
Authentication get() {
return auth;
}
}
}

View File

@@ -22,9 +22,10 @@ public class ResourcesDefinition {
private Phased phased = Phased.before;
public void clear(){
public void clear() {
resources.clear();
}
public void addResource(ResourceDefinition resource, boolean merge) {
ResourceDefinition definition = getResource(resource.getId()).orElse(null);
if (definition != null) {
@@ -44,17 +45,17 @@ public class ResourcesDefinition {
public Optional<ResourceDefinition> getResource(String id) {
return resources
.stream()
.filter(resource -> resource.getId().equals(id))
.findAny();
.stream()
.filter(resource -> resource.getId().equals(id))
.findAny();
}
@JsonIgnore
public List<ResourceDefinition> getDataAccessResources() {
return resources
.stream()
.filter(ResourceDefinition::hasDataAccessAction)
.collect(Collectors.toList());
.stream()
.filter(ResourceDefinition::hasDataAccessAction)
.collect(Collectors.toList());
}
public boolean hasPermission(Permission permission) {
@@ -62,8 +63,8 @@ public class ResourcesDefinition {
return true;
}
return getResource(permission.getId())
.filter(resource -> resource.hasAction(permission.getActions()))
.isPresent();
.filter(resource -> resource.hasAction(permission.getActions()))
.isPresent();
}
public boolean isEmpty() {
@@ -72,18 +73,27 @@ public class ResourcesDefinition {
public boolean hasPermission(Authentication authentication) {
if (CollectionUtils.isEmpty(resources)) {
int size = resources.size();
if (size == 0) {
return true;
}
if (size == 1) {
for (ResourceDefinition resource : resources) {
if (authentication.hasPermission(resource.getId(), resource.getActionIds())) {
return true;
}
}
return false;
}
if (logical == Logical.AND) {
return resources
.stream()
.allMatch(resource -> authentication.hasPermission(resource.getId(), resource.getActionIds()));
.stream()
.allMatch(resource -> authentication.hasPermission(resource.getId(), resource.getActionIds()));
}
return resources
.stream()
.anyMatch(resource -> authentication.hasPermission(resource.getId(), resource.getActionIds()));
.stream()
.anyMatch(resource -> authentication.hasPermission(resource.getId(), resource.getActionIds()));
}
}

View File

@@ -2,7 +2,6 @@ package org.hswebframework.web.authorization.simple;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.hswebframework.web.authorization.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -19,43 +18,38 @@ public class CompositeReactiveAuthenticationManager implements ReactiveAuthentic
@Override
public Mono<Authentication> authenticate(Mono<AuthenticationRequest> request) {
return Flux.concat(providers
.stream()
.map(manager -> manager
.authenticate(request)
.onErrorResume((err) -> {
log.warn("get user authenticate error", err);
return Mono.empty();
}))
.collect(Collectors.toList()))
.take(1)
.next();
return Flux
.concat(
providers
.stream()
.map(manager -> manager
.authenticate(request)
.onErrorResume((err) -> {
log.warn("get user authenticate error", err);
return Mono.empty();
}))
.collect(Collectors.toList()))
.take(1)
.next();
}
@Override
public Mono<Authentication> getByUserId(String userId) {
if (providers.size() == 1) {
return providers.get(0).getByUserId(userId);
}
return Flux
.fromStream(providers
.stream()
.map(manager -> manager
.getByUserId(userId)
.onErrorResume((err) -> {
log.warn("get user [{}] authentication error", userId, err);
return Mono.empty();
})
))
.flatMap(Function.identity())
.collectList()
.filter(CollectionUtils::isNotEmpty)
.map(all -> {
if (all.size() == 1) {
return all.get(0);
}
SimpleAuthentication authentication = new SimpleAuthentication();
for (Authentication auth : all) {
authentication.merge(auth);
}
return authentication;
});
.fromStream(providers
.stream()
.map(manager -> manager
.getByUserId(userId)
.onErrorResume((err) -> {
log.warn("get user [{}] authentication error", userId, err);
return Mono.empty();
})
))
.flatMap(Function.identity())
.as(AuthenticationUtils::merge);
}
}

View File

@@ -24,24 +24,29 @@ import org.hswebframework.web.authorization.*;
import java.io.Serial;
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@Getter
@Setter
public class SimpleAuthentication implements Authentication {
static final AtomicLongFieldUpdater<SimpleAuthentication> ACCESS_COUNT_UPDATER =
AtomicLongFieldUpdater.newUpdater(SimpleAuthentication.class, "accessCount");
@Serial
private static final long serialVersionUID = -2898863220255336528L;
@Getter
private User user;
@Setter
private List<Permission> permissions = new ArrayList<>();
private List<Dimension> dimensions = new ArrayList<>();
@Setter
private Map<String, Serializable> attributes = new HashMap<>();
public static Authentication of() {
@@ -54,9 +59,17 @@ public class SimpleAuthentication implements Authentication {
return Optional.ofNullable((T) attributes.get(name));
}
public List<Dimension> getDimensions() {
return dimensions == null ? Collections.emptyList() : dimensions;
}
public List<Permission> getPermissions() {
return permissions == null ? Collections.emptyList() : permissions;
}
@Override
public Map<String, Serializable> getAttributes() {
return attributes;
return attributes == null ? Collections.emptyMap() : attributes;
}
public SimpleAuthentication merge(Authentication authentication) {
@@ -67,9 +80,10 @@ public class SimpleAuthentication implements Authentication {
if (authentication.getUser() != null) {
user = authentication.getUser();
}
this.attributes = new HashMap<>(getAttributes());
this.attributes.putAll(authentication.getAttributes());
attributes.putAll(authentication.getAttributes());
this.permissions = new ArrayList<>(this.getPermissions());
for (Permission permission : authentication.getPermissions()) {
Permission me = mePermissionGroup.get(permission.getId());
if (me == null) {
@@ -77,11 +91,10 @@ public class SimpleAuthentication implements Authentication {
continue;
}
me.getActions().addAll(permission.getActions());
me.getDataAccesses().addAll(permission.getDataAccesses());
}
this.dimensions = new ArrayList<>(this.getDimensions());
for (Dimension dimension : authentication.getDimensions()) {
if (!getDimension(dimension.getType(), dimension.getId()).isPresent()) {
if (getDimension(dimension.getType(), dimension.getId()).isEmpty()) {
dimensions.add(dimension);
}
}
@@ -96,14 +109,19 @@ public class SimpleAuthentication implements Authentication {
public Authentication copy(BiPredicate<Permission, String> permissionFilter,
Predicate<Dimension> dimension) {
SimpleAuthentication authentication = newInstance();
authentication.setDimensions(dimensions.stream().filter(dimension).collect(Collectors.toList()));
authentication.setDimensions(dimensions
.stream()
.filter(dimension)
.collect(Collectors.toList()));
authentication.setPermissions(permissions
.stream()
.map(permission -> permission.copy(action -> permissionFilter.test(permission, action), conf -> true))
.filter(per -> !per.getActions().isEmpty())
.collect(Collectors.toList())
);
authentication.setUser(user);
if (user != null) {
authentication.setUser0(user);
}
authentication.setAttributes(new HashMap<>(attributes));
return authentication;
}
@@ -128,4 +146,92 @@ public class SimpleAuthentication implements Authentication {
public void addDimension(Dimension dimension) {
this.dimensions.add(dimension);
}
private transient volatile Map<String, Map<String, Dimension>> dimensionMapping;
private transient volatile Map<String, Permission> permissionMapping;
private transient volatile long accessCount;
private boolean fastPath() {
// 总共访问超过8次,则进行初始化缓存.
if (ACCESS_COUNT_UPDATER.incrementAndGet(this) == 8) {
if (permissionMapping == null) {
permissionMapping = permissions == null
? Collections.emptyMap()
: permissions
.stream()
.collect(Collectors
.toMap(Permission::getId,
Function.identity(),
(a, b) -> b));
dimensionMapping = dimensions == null
? Collections.emptyMap()
: dimensions
.stream()
.collect(Collectors
.groupingBy(d -> d.getType().getId(),
Collectors.toMap(Dimension::getId, Function.identity())));
}
}
return permissionMapping != null;
}
@Override
public boolean hasPermission(String permissionId, Collection<String> actions) {
if (fastPath()) {
Permission permission = permissionMapping.get(permissionId);
if (permission == null) {
permission = permissionMapping.get("*");
}
if (permission == null) {
return false;
}
return actions.isEmpty()
|| permission.getActions().containsAll(actions)
|| permission.getActions().contains("*");
}
return Authentication.super.hasPermission(permissionId, actions);
}
@Override
public Optional<Dimension> getDimension(String type, String id) {
if (fastPath()) {
Map<String, Dimension> mapping = dimensionMapping.get(type);
if (mapping == null) {
return Optional.empty();
}
return Optional.ofNullable(mapping.get(id));
}
return Authentication.super.getDimension(type, id);
}
@Override
public Optional<Dimension> getDimension(DimensionType type, String id) {
return getDimension(type.getId(), id);
}
@Override
public List<Dimension> getDimensions(DimensionType type) {
return this.getDimensions(type.getId());
}
@Override
public List<Dimension> getDimensions(String type) {
if (fastPath()) {
Map<String, Dimension> mapping = dimensionMapping.get(type);
if (mapping == null) {
return List.of();
}
return new ArrayList<>(mapping.values());
}
return Authentication.super.getDimensions(type);
}
@Override
public Optional<Permission> getPermission(String id) {
if (fastPath()) {
return Optional.ofNullable(permissionMapping.get(id));
}
return Authentication.super.getPermission(id);
}
}

View File

@@ -37,7 +37,7 @@ public class UserTokenReactiveAuthenticationSupplier implements ReactiveAuthenti
@Override
public Mono<Authentication> get(String userId) {
if (userId == null) {
return null;
return Mono.empty();
}
return get(this.defaultAuthenticationManager, userId);
}

View File

@@ -0,0 +1,856 @@
package org.hswebframework.web.authorization.simple;
import org.hswebframework.web.authorization.*;
import org.hswebframework.web.authorization.DefaultDimensionType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.Serializable;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
class SimpleAuthenticationTest {
private SimpleAuthentication authentication;
private SimpleUser user;
private SimplePermission permission1;
private SimplePermission permission2;
private SimpleDimension dimension1;
private SimpleDimension dimension2;
@BeforeEach
void setUp() {
authentication = new SimpleAuthentication();
// 创建测试用户
user = SimpleUser.builder()
.id("test-user-id")
.username("testuser")
.name("Test User")
.userType("user")
.build();
// 创建测试权限
permission1 = SimplePermission.builder()
.id("permission-1")
.name("Permission 1")
.actions(new HashSet<>(Arrays.asList("query", "save", "delete")))
.build();
permission2 = SimplePermission.builder()
.id("permission-2")
.name("Permission 2")
.actions(new HashSet<>(Arrays.asList("query", "update")))
.build();
// 创建测试维度
SimpleDimensionType orgType = SimpleDimensionType.of("org");
SimpleDimensionType roleType = SimpleDimensionType.of("role");
dimension1 = SimpleDimension.of("org-1", "Organization 1", orgType, null);
dimension2 = SimpleDimension.of("role-1", "Role 1", roleType, null);
}
@Test
void testOf() {
Authentication auth = SimpleAuthentication.of();
assertNotNull(auth);
assertTrue(auth instanceof SimpleAuthentication);
}
@Test
void testSetUser() {
authentication.setUser(user);
assertNotNull(authentication.getUser());
assertEquals("test-user-id", authentication.getUser().getId());
assertEquals("testuser", authentication.getUser().getUsername());
assertEquals("Test User", authentication.getUser().getName());
// setUser 应该自动将用户添加到 dimensions
assertTrue(authentication.getDimensions().contains(user));
}
@Test
void testSetUser0() {
// 使用反射测试 protected 方法
// 注意setUser0 不会将用户添加到 dimensions
// 由于是 protected 方法,这里通过子类或反射测试
// 实际使用中setUser0 通常由子类调用
authentication.setUser(user);
assertEquals(user, authentication.getUser());
}
@Test
void testSetPermissions() {
List<Permission> permissions = Arrays.asList(permission1, permission2);
authentication.setPermissions(permissions);
assertEquals(2, authentication.getPermissions().size());
assertTrue(authentication.getPermissions().contains(permission1));
assertTrue(authentication.getPermissions().contains(permission2));
}
@Test
void testSetDimensions() {
List<Dimension> dimensions = Arrays.asList(dimension1, dimension2);
authentication.setDimensions(dimensions);
assertEquals(2, authentication.getDimensions().size());
assertTrue(authentication.getDimensions().contains(dimension1));
assertTrue(authentication.getDimensions().contains(dimension2));
}
@Test
void testSetDimensionsCollection() {
Collection<Dimension> dimensions = new HashSet<>(Arrays.asList(dimension1, dimension2));
authentication.setDimensions(dimensions);
assertEquals(2, authentication.getDimensions().size());
}
@Test
void testAddDimension() {
authentication.addDimension(dimension1);
authentication.addDimension(dimension2);
assertEquals(2, authentication.getDimensions().size());
assertTrue(authentication.getDimensions().contains(dimension1));
assertTrue(authentication.getDimensions().contains(dimension2));
}
@Test
void testSetAttributes() {
Map<String, Serializable> attributes = new HashMap<>();
attributes.put("key1", "value1");
attributes.put("key2", 123);
authentication.setAttributes(attributes);
assertEquals(2, authentication.getAttributes().size());
assertEquals("value1", authentication.getAttributes().get("key1"));
assertEquals(123, authentication.getAttributes().get("key2"));
}
@Test
void testGetAttribute() {
authentication.setAttributes(Collections.singletonMap("test-key", "test-value"));
Optional<String> value = authentication.getAttribute("test-key");
assertTrue(value.isPresent());
assertEquals("test-value", value.get());
Optional<String> missing = authentication.getAttribute("missing-key");
assertFalse(missing.isPresent());
}
@Test
void testGetAttributes() {
Map<String, Serializable> attributes = new HashMap<>();
attributes.put("key1", "value1");
authentication.setAttributes(attributes);
Map<String, Serializable> result = authentication.getAttributes();
assertNotNull(result);
assertEquals("value1", result.get("key1"));
}
@Test
void testHasPermission() {
authentication.setPermissions(Arrays.asList(permission1, permission2));
// 测试有权限的情况
assertTrue(authentication.hasPermission("permission-1", Collections.singletonList("query")));
assertTrue(authentication.hasPermission("permission-1", Arrays.asList("query", "save")));
assertTrue(authentication.hasPermission("permission-2", Collections.singletonList("query")));
// 测试没有权限的情况
assertFalse(authentication.hasPermission("permission-1", Collections.singletonList("unknown")));
assertFalse(authentication.hasPermission("unknown-permission", Collections.singletonList("query")));
// 测试空 actions 列表
assertTrue(authentication.hasPermission("permission-1", Collections.emptyList()));
}
@Test
void testHasPermissionWithWildcard() {
SimplePermission wildcardPermission = SimplePermission.builder()
.id("*")
.name("All Permissions")
.actions(new HashSet<>(Collections.singletonList("*")))
.build();
authentication.setPermissions(Collections.singletonList(wildcardPermission));
// 通配符权限应该允许所有操作
assertTrue(authentication.hasPermission("any-permission", Collections.singletonList("any-action")));
}
@Test
void testHasPermissionWithActionWildcard() {
SimplePermission permissionWithWildcard = SimplePermission.builder()
.id("permission-1")
.name("Permission with wildcard")
.actions(new HashSet<>(Collections.singletonList("*")))
.build();
authentication.setPermissions(Collections.singletonList(permissionWithWildcard));
// 权限包含 * action 应该允许所有操作
assertTrue(authentication.hasPermission("permission-1", Collections.singletonList("any-action")));
assertTrue(authentication.hasPermission("permission-1", Arrays.asList("action1", "action2")));
}
@Test
void testGetPermission() {
authentication.setPermissions(Arrays.asList(permission1, permission2));
Optional<Permission> perm1 = authentication.getPermission("permission-1");
assertTrue(perm1.isPresent());
assertEquals("permission-1", perm1.get().getId());
Optional<Permission> perm2 = authentication.getPermission("permission-2");
assertTrue(perm2.isPresent());
assertEquals("permission-2", perm2.get().getId());
Optional<Permission> missing = authentication.getPermission("unknown");
assertFalse(missing.isPresent());
}
@Test
void testGetDimension() {
authentication.setDimensions(Arrays.asList(dimension1, dimension2));
Optional<Dimension> dim1 = authentication.getDimension("org", "org-1");
assertTrue(dim1.isPresent());
assertEquals("org-1", dim1.get().getId());
Optional<Dimension> dim2 = authentication.getDimension("role", "role-1");
assertTrue(dim2.isPresent());
assertEquals("role-1", dim2.get().getId());
Optional<Dimension> missing = authentication.getDimension("org", "unknown");
assertFalse(missing.isPresent());
}
@Test
void testGetDimensionWithDimensionType() {
authentication.setDimensions(Arrays.asList(dimension1, dimension2));
SimpleDimensionType orgType = SimpleDimensionType.of("org");
Optional<Dimension> dim = authentication.getDimension(orgType, "org-1");
assertTrue(dim.isPresent());
assertEquals("org-1", dim.get().getId());
}
@Test
void testGetDimensions() {
SimpleDimension org2 = SimpleDimension.of("org-2", "Organization 2", SimpleDimensionType.of("org"), null);
authentication.setDimensions(Arrays.asList(dimension1, org2, dimension2));
List<Dimension> orgDimensions = authentication.getDimensions("org");
assertEquals(2, orgDimensions.size());
List<Dimension> roleDimensions = authentication.getDimensions("role");
assertEquals(1, roleDimensions.size());
assertEquals("role-1", roleDimensions.get(0).getId());
List<Dimension> unknownDimensions = authentication.getDimensions("unknown");
assertTrue(unknownDimensions.isEmpty());
}
@Test
void testGetDimensionsWithDimensionType() {
authentication.setDimensions(Arrays.asList(dimension1, dimension2));
SimpleDimensionType orgType = SimpleDimensionType.of("org");
List<Dimension> dimensions = authentication.getDimensions(orgType);
assertEquals(1, dimensions.size());
assertEquals("org-1", dimensions.get(0).getId());
}
@Test
void testHasDimension() {
authentication.setDimensions(Arrays.asList(dimension1, dimension2));
assertTrue(authentication.hasDimension("org", "org-1"));
assertTrue(authentication.hasDimension("role", "role-1"));
assertFalse(authentication.hasDimension("org", "unknown"));
assertFalse(authentication.hasDimension("unknown", "org-1"));
}
@Test
void testMerge() {
// 设置初始认证信息
authentication.setUser(user);
authentication.setPermissions(Collections.singletonList(permission1));
authentication.setDimensions(Collections.singletonList(dimension1));
authentication.setAttributes(Collections.singletonMap("key1", "value1"));
// 创建要合并的认证信息
SimpleAuthentication other = new SimpleAuthentication();
SimpleUser otherUser = SimpleUser.builder()
.id("other-user-id")
.username("otheruser")
.build();
other.setUser(otherUser);
other.setPermissions(Collections.singletonList(permission2));
other.setDimensions(Collections.singletonList(dimension2));
other.setAttributes(Collections.singletonMap("key2", "value2"));
// 执行合并
SimpleAuthentication merged = authentication.merge(other);
// 验证用户被更新
assertEquals("other-user-id", merged.getUser().getId());
// 验证权限被合并permission1 和 permission2 都应该存在)
assertEquals(2, merged.getPermissions().size());
// 验证维度被合并(不重复添加)
assertTrue(merged.getDimensions().contains(dimension1));
assertTrue(merged.getDimensions().contains(dimension2));
// 验证属性被合并
assertEquals(2, merged.getAttributes().size());
assertEquals("value1", merged.getAttributes().get("key1"));
assertEquals("value2", merged.getAttributes().get("key2"));
}
@Test
void testMergeWithDuplicatePermissions() {
// 设置初始权限
authentication.setPermissions(Collections.singletonList(permission1));
// 创建具有相同 ID 但不同 actions 的权限
SimplePermission permission1WithMoreActions = SimplePermission.builder()
.id("permission-1")
.name("Permission 1")
.actions(new HashSet<>(Arrays.asList("query", "save", "delete", "update")))
.build();
SimpleAuthentication other = new SimpleAuthentication();
other.setPermissions(Collections.singletonList(permission1WithMoreActions));
// 执行合并
SimpleAuthentication merged = authentication.merge(other);
// 验证权限被合并actions 被合并
assertEquals(1, merged.getPermissions().size());
Permission mergedPermission = merged.getPermissions().get(0);
assertEquals("permission-1", mergedPermission.getId());
assertTrue(mergedPermission.getActions().contains("query"));
assertTrue(mergedPermission.getActions().contains("save"));
assertTrue(mergedPermission.getActions().contains("delete"));
assertTrue(mergedPermission.getActions().contains("update"));
}
@Test
void testMergeWithDuplicateDimensions() {
authentication.setDimensions(Collections.singletonList(dimension1));
SimpleAuthentication other = new SimpleAuthentication();
other.setDimensions(Collections.singletonList(dimension1)); // 相同的维度
SimpleAuthentication merged = authentication.merge(other);
// 验证维度不会被重复添加
long org1Count = merged.getDimensions().stream()
.filter(d -> d.getId().equals("org-1") && d.getType().getId().equals("org"))
.count();
assertEquals(1, org1Count);
}
@Test
void testMergeWithNullUser() {
authentication.setUser(user);
SimpleAuthentication other = new SimpleAuthentication();
// other 没有设置用户
SimpleAuthentication merged = authentication.merge(other);
// 验证原始用户保持不变
assertEquals(user, merged.getUser());
}
@Test
void testCopy() {
authentication.setUser(user);
authentication.setPermissions(Arrays.asList(permission1, permission2));
authentication.setDimensions(Arrays.asList(dimension1, dimension2));
authentication.setAttributes(Collections.singletonMap("key1", "value1"));
// 复制所有权限和维度
Authentication copied = authentication.copy(
(permission, action) -> true, // 允许所有权限和操作
dimension -> true // 允许所有维度
);
assertNotNull(copied);
assertEquals(user, copied.getUser());
assertEquals(2, copied.getPermissions().size());
// user,org,role
assertEquals(3, copied.getDimensions().size());
assertEquals("value1", copied.getAttributes().get("key1"));
}
@Test
void testCopyWithPermissionFilter() {
authentication.setPermissions(Arrays.asList(permission1, permission2));
// 只复制 permission-1
Authentication copied = authentication.copy(
(permission, action) -> permission.getId().equals("permission-1"),
dimension -> true
);
assertEquals(1, copied.getPermissions().size());
assertEquals("permission-1", copied.getPermissions().get(0).getId());
}
@Test
void testCopyWithActionFilter() {
authentication.setPermissions(Collections.singletonList(permission1));
// 只复制 query action
Authentication copied = authentication.copy(
(permission, action) -> action.equals("query"),
dimension -> true
);
assertEquals(1, copied.getPermissions().size());
Permission copiedPermission = copied.getPermissions().get(0);
assertEquals("permission-1", copiedPermission.getId());
assertEquals(1, copiedPermission.getActions().size());
assertTrue(copiedPermission.getActions().contains("query"));
assertFalse(copiedPermission.getActions().contains("save"));
}
@Test
void testCopyWithDimensionFilter() {
authentication.setDimensions(Arrays.asList(dimension1, dimension2));
// 只复制 org 类型的维度
Authentication copied = authentication.copy(
(permission, action) -> true,
dimension -> dimension.getType().getId().equals("org")
);
assertEquals(1, copied.getDimensions(dimension1.getType()).size());
assertEquals("org-1", copied.getDimensions().get(0).getId());
}
@Test
void testCopyFiltersEmptyActions() {
SimplePermission permissionWithEmptyActions = SimplePermission.builder()
.id("empty-permission")
.name("Empty Permission")
.actions(new HashSet<>())
.build();
authentication.setPermissions(Collections.singletonList(permissionWithEmptyActions));
// 复制时,如果过滤后 actions 为空,权限应该被过滤掉
Authentication copied = authentication.copy(
(permission, action) -> false, // 不允许任何 action
dimension -> true
);
assertEquals(0, copied.getPermissions().size());
}
@Test
void testFastPathOptimization() {
authentication.setPermissions(Collections.singletonList(permission1));
authentication.setDimensions(Collections.singletonList(dimension1));
// 前7次访问应该使用慢路径
for (int i = 0; i < 7; i++) {
authentication.hasPermission("permission-1", Collections.singletonList("query"));
}
// 第8次访问应该触发快速路径初始化
assertTrue(authentication.hasPermission("permission-1", Collections.singletonList("query")));
// 之后的访问应该使用快速路径
assertTrue(authentication.hasPermission("permission-1", Collections.singletonList("query")));
assertTrue(authentication.getPermission("permission-1").isPresent());
assertTrue(authentication.getDimension("org", "org-1").isPresent());
}
@Test
void testNewInstance() {
SimpleAuthentication instance1 = authentication.newInstance();
SimpleAuthentication instance2 = authentication.newInstance();
assertNotNull(instance1);
assertNotNull(instance2);
assertNotSame(instance1, instance2);
assertTrue(instance1 instanceof SimpleAuthentication);
assertTrue(instance2 instanceof SimpleAuthentication);
}
@Test
void testEmptyPermissions() {
authentication.setPermissions(Collections.emptyList());
assertFalse(authentication.hasPermission("any", Collections.singletonList("any")));
assertFalse(authentication.getPermission("any").isPresent());
}
@Test
void testEmptyDimensions() {
authentication.setDimensions(Collections.emptyList());
assertFalse(authentication.hasDimension("any", "any"));
assertFalse(authentication.getDimension("any", "any").isPresent());
assertTrue(authentication.getDimensions("any").isEmpty());
}
@Test
void testNullAttributes() {
// 测试 null 属性处理
authentication.setAttributes(null);
assertNotNull(authentication.getAttributes());
}
@Test
void testGetAttributeWithType() {
authentication.setAttributes(Collections.singletonMap("int-value", 123));
Optional<Integer> intValue = authentication.getAttribute("int-value");
assertTrue(intValue.isPresent());
assertEquals(123, intValue.get());
}
@Test
void testMultipleDimensionsSameType() {
SimpleDimension org2 = SimpleDimension.of("org-2", "Organization 2", SimpleDimensionType.of("org"), null);
SimpleDimension org3 = SimpleDimension.of("org-3", "Organization 3", SimpleDimensionType.of("org"), null);
authentication.setDimensions(Arrays.asList(dimension1, org2, org3));
List<Dimension> orgDimensions = authentication.getDimensions("org");
assertEquals(3, orgDimensions.size());
}
@Test
void testUserAsDimension() {
authentication.setUser(user);
// 用户应该被添加到维度列表中
assertTrue(authentication.getDimensions().contains(user));
// 可以通过维度类型查找用户
Optional<Dimension> userDimension = authentication.getDimension(
DefaultDimensionType.user.getId(),
user.getId()
);
assertTrue(userDimension.isPresent());
}
// ========== 性能测试 ==========
@Test
void testPerformanceBeforeFastPath() {
// 准备大量权限和维度数据
List<Permission> permissions = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
permissions.add(SimplePermission.builder()
.id("permission-" + i)
.name("Permission " + i)
.actions(new HashSet<>(Arrays.asList("query", "save", "delete")))
.build());
}
List<Dimension> dimensions = new ArrayList<>();
for (int i = 0; i < 500; i++) {
dimensions.add(SimpleDimension.of(
"dim-" + i,
"Dimension " + i,
SimpleDimensionType.of("type-" + (i % 10)),
null
));
}
int iterations = 10000;
long totalTime = 0;
// 使用多个实例来测试慢路径每个实例只访问7次
int batchSize = 7;
int batches = iterations / batchSize;
long startTime = System.nanoTime();
for (int batch = 0; batch < batches; batch++) {
SimpleAuthentication auth = new SimpleAuthentication();
auth.setUser(user);
auth.setPermissions(permissions);
auth.setDimensions(dimensions);
// 每个实例只访问7次fastPath 未生效)
for (int i = 0; i < batchSize; i++) {
int idx = (batch * batchSize + i) % 1000;
auth.hasPermission("permission-" + idx, Collections.singletonList("query"));
auth.getPermission("permission-" + idx);
auth.getDimension("type-5", "dim-" + (idx % 500));
auth.getDimensions("type-5");
}
}
long endTime = System.nanoTime();
totalTime = endTime - startTime;
double avgTimeNanos = (double) totalTime / iterations;
double opsPerSecond = 1_000_000_000.0 / avgTimeNanos;
System.out.println("\n========== FastPath 生效前性能测试 ==========");
System.out.println("迭代次数: " + iterations);
System.out.println("总耗时: " + (totalTime / 1_000_000) + " ms");
System.out.println("平均每次操作耗时: " + String.format("%.2f", avgTimeNanos / 1000) + " μs");
System.out.println("每秒操作数: " + String.format("%.2f", opsPerSecond / 4) + " ops/s (每个方法)");
System.out.println("==========================================\n");
}
@Test
void testPerformanceAfterFastPath() {
// 准备大量权限和维度数据
List<Permission> permissions = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
permissions.add(SimplePermission.builder()
.id("permission-" + i)
.name("Permission " + i)
.actions(new HashSet<>(Arrays.asList("query", "save", "delete")))
.build());
}
List<Dimension> dimensions = new ArrayList<>();
for (int i = 0; i < 500; i++) {
dimensions.add(SimpleDimension.of(
"dim-" + i,
"Dimension " + i,
SimpleDimensionType.of("type-" + (i % 10)),
null
));
}
SimpleAuthentication auth = new SimpleAuthentication();
auth.setUser(user);
auth.setPermissions(permissions);
auth.setDimensions(dimensions);
// 触发 fastPath 初始化访问8次
for (int i = 0; i < 8; i++) {
auth.hasPermission("permission-500", Collections.singletonList("query"));
}
int iterations = 10000;
long totalTime = 0;
// 测试 fastPath 生效后的性能(快路径)
long startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
auth.hasPermission("permission-" + (i % 1000), Collections.singletonList("query"));
auth.getPermission("permission-" + (i % 1000));
auth.getDimension("type-5", "dim-" + (i % 500));
auth.getDimensions("type-5");
}
long endTime = System.nanoTime();
totalTime = endTime - startTime;
double avgTimeNanos = (double) totalTime / iterations;
double opsPerSecond = 1_000_000_000.0 / avgTimeNanos;
System.out.println("\n========== FastPath 生效后性能测试 ==========");
System.out.println("迭代次数: " + iterations);
System.out.println("总耗时: " + (totalTime / 1_000_000) + " ms");
System.out.println("平均每次操作耗时: " + String.format("%.2f", avgTimeNanos / 1000) + " μs");
System.out.println("每秒操作数: " + String.format("%.2f", opsPerSecond / 4) + " ops/s (每个方法)");
System.out.println("==========================================\n");
}
@Test
void testPerformanceComparison() {
// 准备大量权限和维度数据
List<Permission> permissions = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
permissions.add(SimplePermission.builder()
.id("permission-" + i)
.name("Permission " + i)
.actions(new HashSet<>(Arrays.asList("query", "save", "delete")))
.build());
}
List<Dimension> dimensions = new ArrayList<>();
for (int i = 0; i < 500; i++) {
dimensions.add(SimpleDimension.of(
"dim-" + i,
"Dimension " + i,
SimpleDimensionType.of("type-" + (i % 10)),
null
));
}
int iterations = 10000;
// 测试慢路径性能
SimpleAuthentication slowPathAuth = new SimpleAuthentication();
slowPathAuth.setUser(user);
slowPathAuth.setPermissions(permissions);
slowPathAuth.setDimensions(dimensions);
// 只访问7次确保 fastPath 不生效
for (int i = 0; i < 7; i++) {
slowPathAuth.hasPermission("permission-500", Collections.singletonList("query"));
}
long slowPathStart = System.nanoTime();
for (int i = 0; i < iterations; i++) {
slowPathAuth.hasPermission("permission-" + (i % 1000), Collections.singletonList("query"));
slowPathAuth.getPermission("permission-" + (i % 1000));
slowPathAuth.getDimension("type-5", "dim-" + (i % 500));
slowPathAuth.getDimensions("type-5");
}
long slowPathTime = System.nanoTime() - slowPathStart;
// 测试快路径性能
SimpleAuthentication fastPathAuth = new SimpleAuthentication();
fastPathAuth.setUser(user);
fastPathAuth.setPermissions(permissions);
fastPathAuth.setDimensions(dimensions);
// 触发 fastPath 初始化访问8次
for (int i = 0; i < 8; i++) {
fastPathAuth.hasPermission("permission-500", Collections.singletonList("query"));
}
long fastPathStart = System.nanoTime();
for (int i = 0; i < iterations; i++) {
fastPathAuth.hasPermission("permission-" + (i % 1000), Collections.singletonList("query"));
fastPathAuth.getPermission("permission-" + (i % 1000));
fastPathAuth.getDimension("type-5", "dim-" + (i % 500));
fastPathAuth.getDimensions("type-5");
}
long fastPathTime = System.nanoTime() - fastPathStart;
// 计算性能提升
double slowPathAvg = (double) slowPathTime / iterations;
double fastPathAvg = (double) fastPathTime / iterations;
double improvement = ((slowPathAvg - fastPathAvg) / slowPathAvg) * 100;
System.out.println("\n========== FastPath 性能对比测试 ==========");
System.out.println("测试数据规模:");
System.out.println(" - 权限数量: 1000");
System.out.println(" - 维度数量: 500");
System.out.println(" - 迭代次数: " + iterations);
System.out.println();
System.out.println("慢路径 (FastPath 未生效):");
System.out.println(" - 总耗时: " + (slowPathTime / 1_000_000) + " ms");
System.out.println(" - 平均每次操作: " + String.format("%.2f", slowPathAvg / 1000) + " μs");
System.out.println();
System.out.println("快路径 (FastPath 已生效):");
System.out.println(" - 总耗时: " + (fastPathTime / 1_000_000) + " ms");
System.out.println(" - 平均每次操作: " + String.format("%.2f", fastPathAvg / 1000) + " μs");
System.out.println();
System.out.println("性能提升: " + String.format("%.2f", improvement) + "%");
System.out.println("性能倍数: " + String.format("%.2f", slowPathAvg / fastPathAvg) + "x");
System.out.println("==========================================\n");
// 验证 fastPath 确实提升了性能
assertTrue(fastPathTime < slowPathTime,
"FastPath 应该比慢路径更快。慢路径: " + slowPathTime + " ns, 快路径: " + fastPathTime + " ns");
}
@Test
void testPerformanceWithDifferentDataSizes() {
int[] permissionSizes = {100, 500, 1000, 2000};
int[] dimensionSizes = {50, 250, 500, 1000};
int iterations = 5000;
System.out.println("\n========== 不同数据规模下的性能测试 ==========");
System.out.println("迭代次数: " + iterations);
System.out.println();
for (int permSize : permissionSizes) {
for (int dimSize : dimensionSizes) {
// 准备数据
List<Permission> permissions = new ArrayList<>();
for (int i = 0; i < permSize; i++) {
permissions.add(SimplePermission.builder()
.id("permission-" + i)
.name("Permission " + i)
.actions(new HashSet<>(Arrays.asList("query", "save")))
.build());
}
List<Dimension> dimensions = new ArrayList<>();
for (int i = 0; i < dimSize; i++) {
dimensions.add(SimpleDimension.of(
"dim-" + i,
"Dimension " + i,
SimpleDimensionType.of("type-" + (i % 10)),
null
));
}
SimpleAuthentication auth = new SimpleAuthentication();
auth.setUser(user);
auth.setPermissions(permissions);
auth.setDimensions(dimensions);
// 触发 fastPath
for (int i = 0; i < 8; i++) {
auth.hasPermission("permission-0", Collections.singletonList("query"));
}
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
auth.hasPermission("permission-" + (i % permSize), Collections.singletonList("query"));
auth.getPermission("permission-" + (i % permSize));
auth.getDimension("type-0", "dim-" + (i % dimSize));
auth.getDimensions("type-0");
}
long time = System.nanoTime() - start;
double avgTime = (double) time / iterations;
System.out.println(String.format(
"权限: %4d, 维度: %4d -> 总耗时: %6.2f ms, 平均: %6.2f μs/op",
permSize, dimSize, time / 1_000_000.0, avgTime / 1000.0
));
}
}
System.out.println("==========================================\n");
}
@Test
void testFastPathInitializationThreshold() {
authentication.setPermissions(Collections.singletonList(permission1));
authentication.setDimensions(Collections.singletonList(dimension1));
// 验证前7次访问不会初始化 fastPath
for (int i = 0; i < 7; i++) {
authentication.hasPermission("permission-1", Collections.singletonList("query"));
}
// 第8次访问应该触发 fastPath 初始化
long beforeInit = System.nanoTime();
authentication.hasPermission("permission-1", Collections.singletonList("query"));
long initTime = System.nanoTime() - beforeInit;
// 第9次及之后的访问应该使用 fastPath
long afterInit = System.nanoTime();
for (int i = 0; i < 100; i++) {
authentication.hasPermission("permission-1", Collections.singletonList("query"));
}
long fastPathTime = System.nanoTime() - afterInit;
System.out.println("\n========== FastPath 初始化阈值测试 ==========");
System.out.println("第8次访问耗时包含初始化: " + (initTime / 1000) + " μs");
System.out.println("后续100次访问总耗时: " + (fastPathTime / 1_000_000) + " ms");
System.out.println("后续100次访问平均耗时: " + (fastPathTime / 100_000.0) + " μs");
System.out.println("==========================================\n");
// 验证初始化后的访问确实更快
assertTrue(fastPathTime / 100.0 < initTime * 10,
"FastPath 初始化后的访问应该比初始化时更快");
}
}

View File

@@ -55,7 +55,7 @@ public class AuthorizingHandlerAutoConfiguration {
@Bean
public ReactiveAuthenticationManagerProvider embedAuthenticationManager(EmbedAuthenticationProperties properties) {
return new EmbedReactiveAuthenticationManager(properties);
return properties.getUsers().isEmpty() ? null : new EmbedReactiveAuthenticationManager(properties);
}
@Bean