最近,我正在开发一个严重依赖各种数据源的应用程序。 我们的应用程序实际上是对数据进行组合、解释和可视化。
并非所有人都可以查看所有数据。 我们需要一种简单的方法来保护对我们资源的访问,尽管不仅仅基于典型的用户和角色。 应当更细化地保护对数据的访问。 但是,一方面访问规则是由一组互斥的参数定义的,但我们也希望保持代码尽可能的干净和可维护。
由于我们的应用程序已经依赖于Spring Boot,因此我们的选择取决于spring-security,更确切地说,它是@PreAuthorize批注。
我们本可以选择复制/粘贴/适应随附的SPeL表达式,尽管这样会很快引入维护问题,例如, 假设我们只有2个不同的参数用于授予对一条数据的访问权限,那么我们仍将在整个代码中复制SPeL表达式,每当访问规则发生更改时都会引入维护问题。
@Component
public class SomeSecuredResource {
@PreAuthorize("hasAccess(#foo)")
public SensitiveData showMe(Foo foo){
return new SensitiveData();
}
@PreAuthorize("isAllowedToSee(#bar)")
public SensitiveData showMeToo(Bar bar){
return new SensitiveData();
}
}
所以我们为此创建了一个简单的注释
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@PreAuthorize(Sensitive.EXPRESSION)
public @interface Sensitive {
String EXPRESSION = "#foo != null ? hasAccess(#foo) : (#bar != null ? isAllowedToSee(#bar) : false)";
}
我们现在可以应用:
@Component
public class SomeSecuredResource {
@Sensitivepublic SensitiveData showMe(Foo foo){
return new SensitiveData();
}
@Sensitivepublic SensitiveData showMeToo(Bar bar){
return new SensitiveData();
}
}
很好。问题解决了,人们会认为……
这只是部分正确。 好的,我们隔离了在给定特定方法参数的情况下允许访问方法的逻辑,但是引入了其他不那么明显的问题。
假设有新的开发人员加入团队。 而且他看到了这些@Sensitive批注保护了对资源的访问。 他将它们应用于:
@Component
public class NewDevsClass {
@Sensitivepublic SensitiveData doSomething(long someParameter){
return new SensitiveData();
}
@Sensitivepublic SensitiveData doOtherStuff(String foo){
return new SensitiveData();
}
}
他在这里违反了隐性规则。 如我们所见,@ Sensitive批注的实现依赖于Foo或Bar类型的参数。
实际上,他违反了我们的体系结构规则,即:
用@Sensitive注释注释的方法必须具有Foo或Bar类型的参数。
那么,我们该如何解决呢? 在Wiki上保留大量的规则列表,并让每个人都对其进行筛选吗? 配置Sonar规则并每晚收集报告? 否,将这些规则嵌入快速执行的单元测试中并获得即时反馈会怎么样?
请大家欢迎 ArchUnit
ArchUnit:一个Java体系结构测试库,用于以纯Java指定和声明体系结构规则。因此,让我们深入研究并编写测试来确保正确使用我们的注释。
public class SensitiveAnnotationUsageTest {
DescribedPredicate<JavaClass> haveAFieldAnnotatedWithSensitive =
new DescribedPredicate<JavaClass>("have a field annotated with @Sensitive"){
@Overridepublic boolean apply(JavaClass input) {
// note : simplified version which inspects all classesreturn true;
}
};
ArchCondition<JavaClass> mustContainAParameterOfTypeFooOrBar =
new ArchCondition<JavaClass>("must have parameter of type 'com.example.post.Foo' or 'com.example.post.Bar'") {
@Overridepublic void check(JavaClass item, ConditionEvents events) {
List<JavaMethod> collect = item.getMethods().stream()
.filter(method -> method.isAnnotatedWith(Sensitive.class)).collect(Collectors.toList());
for(JavaMethod method: collect){
List<String> names = method.getParameters().getNames();
if(!names.contains("com.example.post.Foo") && !names.contains("com.example.post.Bar")) {
String message = String.format(
"Method %s bevat geen parameter met type 'Foo' of 'Bar", method.getFullName());
events.add(SimpleConditionEvent.violated(method, message));
}
}
}
};
@Testpublic void checkArchitecturalRules(){
JavaClasses importedClasses = new ClassFileImporter().importPackages("com.example.post");
ArchRule rule = ArchRuleDefinition.classes()
.that(haveAFieldAnnotatedWithSensitive)
.should(mustContainAParameterOfTypeFooOrBar);
rule.check(importedClasses);
}
}
在我们的2个类上执行此测试将得出以下结果:
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that have a field annotated with @Sensitive should must have parameter of type 'com.example.post.Foo' was violated (2 times):
Method com.example.post.NewDevsClass.doOtherStuff(java.lang.String) bevat geen parameter met type 'Foo' of 'Bar
Method com.example.post.NewDevsClass.doSomething(long) bevat geen parameter met type 'Foo' of 'Bar
看! 我们已经进行了测试,并且可以正确应用我们的架构规则。