⬆️ ⬇️

Declarative security with annotations and AspectJ in Java SE

The purpose of this article is to write a mini container framework that will take over the task of authorization and user authentication, allowing you to write a minimum of code in the client program. Immediately make a reservation, in real life I use Spring Security (previously the project was called Acegi).

The article is intended for those who want to demystify for themselves the magic of the work of such decisions, or for those who for some reason do not consider it appropriate to use public solutions and plans to create their own implementation of the security system. One of these reasons is the limited amount of RAM and the inability to run “adult” frameworks, for example, in a Java environment like Android (I do not have data on the successful use of AspectJ on the Android platform, but this is only a matter of time).



I do not know how in other teams, we reluctantly use annotations, although this innovation was added to the Java language in version 1.5. It seems like 7 years have passed. (and almost 10 years after the premiere of Microsoft .NET - there the annotations (attributes) were originally). I admit, I do not know anyone who would write them himself. Mostly people use libraries in which they already exist. Perhaps because not many are aware of the situations in which they can actually be useful. 7 years is enough time to understand - annotations are not ephemeral, a cry of fashion. It seems that they are not going to disappear from Java, and from C # too. I think that they will remain in Java for a long time.



Annotations have a significant drawback: so that at run time a container (in the simplest case, some method that returns an object created using new and does some actions before it), could find an annotated method, a method parameter, or a class field, must cycle through all methods and fields, checking if they are annotated. This increases the time of initialization of the object and the launch of the program. Typically, the metadata contained in the annotations that a particular type has is cached; however, there is another way to use annotations without significantly increasing the object's initialization time.



Let's start with the final result. For a small desktop application, I needed to make a security system — checking user rights to perform specific methods. By this time - after successful experiences with EJB3, JBoss Seam and Spring - I had already managed to get used to some declarative code writing techniques, without clogging up the program code with different service nonsense (from the point of view of clogging up the main program logic) such as checking user rights, defining transaction boundaries and link dependency management. I decided that just like in EJB3 and Spring Security in my program, the method above which the Allow annotation (ERole.ADMIN) is executed will be executed only if the user (the current execution flow) has the rights specified in the Allow annotation parameter, that is, ERole.ADMIN. For the sake of simplicity, examples of rights will be only two types: Allow (ERole.USER) and Allow (ERole.ADMIN).

')

This is how one of the example classes will look like:

package eu.vitaliy.testaspectjsecurity;



public class ClassA {

public void mWithoutPermission()

{

System. out .println( "Method ClassA.mWithoutPermission()" );

}



@Allow({ ERole.USER, ERole.ADMIN})

public void mUserAndAdmin()

{

System. out .println( "Method ClassA.mUser()" );

}



@Allow(ERole.ADMIN)

public void mAdmin()

{

System. out .println( "Method ClassA.mAdmin()" );

}

}



An alternative is possible when we annotate the whole class. Then all the methods of the class need appropriate rights, and we can override those methods for the execution of which requires special rights.

package eu.vitaliy.testaspectjsecurity2;

import eu.vitaliy.testaspectjsecurity.ERole;

import eu.vitaliy.testaspectjsecurity.Allow;



@Allow(ERole.USER)

public class ClassB {



public void mUser1()

{

System. out .println( "Method ClassB.mUser1()" );

}



public void mUser2()

{

System. out .println( "Method ClassB.mUser2()" );

}



@Allow(ERole.ADMIN)

private void mAdmin()

{

System. out .println( "Method ClassB.mAdmin()" );

}

}


Here is the definition of annotations:

package eu.vitaliy.testaspectjsecurity;



import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;



@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.METHOD, ElementType.TYPE})

public @ interface Allow {

ERole[] value () default {ERole.USER};

}




It depends on the ERole listing:

package eu.vitaliy.testaspectjsecurity;



public enum ERole {

ADMIN,

USER

}




The example will use a simple PermissionStore in which user rights will be stored:



The example uses a simple PermissionStore in which user rights will be stored. If the user does not have the appropriate rights, a MySecurityException will be thrown which is derived from java.lang.RuntimeException:

package eu.vitaliy.testaspectjsecurity;



import java.util.HashSet;

import java.util.Set;



public class PermissionStore {



private static Set<ERole> permissions = new HashSet<ERole>();



public static void addPermission(ERole role)

{

permissions.add(role);

}



public static boolean check(ERole role)

{

return permissions.contains(role);

}

}




And this is how the main method looks like:

package eu.vitaliy.testaspectjsecurity;



import eu.vitaliy.testaspectjsecurity2.ClassB;



public class Main {

public static void main( String [] args)

{



/*

* ,

* MySecurityException

*/

PermissionStore.addPermission(ERole.USER);

PermissionStore.addPermission(ERole.ADMIN);



ClassA a = new ClassA();

a.mUserAndAdmin();

a.mWithoutPermission();

a.mAdmin();



ClassB = new ClassB();

c.mUser1();

c.mUser2();



}

}




In order for the program to run to the end, the user must have ERole.USER and ERole.ADMIN rights.



Now about implementation. As you might have guessed, the user rights verification code that throws a MySecurityException will not work just like that. Traditionally, an object that is configured using an annotation is created using a container factory, because creating an object using the new operator would have to take additional steps to verify user rights. The technology that helps to avoid scanning the class for annotations is called AspectJ, but first it’s worth doing a little review like it did before, without AspectJ. If you are not very interested, you can go to method 3 - implementation using AspectJ.



Method 1 Java Reflection API (starting with J2SE 1.3): Create a dynamic proxy object that implements the same interfaces as our class using java.lang reflect.Proxy and java.lang.reflect.InvocationHandler . In the Object method java.lang.reflect.InvocationHandler.invoke (Object proxy, Method method, Object [] args) check whether the method is annotated with the Allow annotation and depending on the content value and a certain PermissionStore (where the information is stored, what rights do you have user) throw (or not) the exception MySecurutyException.

The disadvantages of this method are:

1. Overhead to create an object.

2. The overhead of calling each method.

3. The class must implement the interface.

4. The invoke method of the InvocationHandler class will be invoked for all methods, including those that do not need a security check. If there are a small number of methods in the class that require verification, there is another useless overhead.



Method 2 CGLIB library is a library for generating code bytes at runtime.

It has a significantly faster implementation of calling the object's proxy methods. In principle, it is not something more revolutionary than reflection. The disadvantages are the same as for method 1, except that it allows you to create a proxy object that does not implement any interfaces.



Method 3 AspectJ is a Java language extension that adds aspect programming to it. The resulting code remains fully compatible with the standard Java virtual machine. AspectJ is able to insert a security verification code at compile time, finding the methods we are interested in using some mask that we can set, and wildcards are allowed - the characters used to replace one or more other characters. Starting with a certain version of AspectJ, a Java annotation may be included in the mask, which has become much more convenient since I used to prefix methods so that security advice (advice) could be applied to them, for example:

* * .secure * (..)

As I said above, the verification code is inserted at compile time. This is accomplished by the aspectbench compiler, whose input is compiled byte code, which is undoubtedly convenient, since it is possible to aspect a commercial code without source codes, including for modifying the logic of its operation. Because of this, this approach is free from the disadvantages of 1, 2 and 4 of the first method.



The main syntactic unit of the AspectJ language is aspect. An aspect is a set of rules (advice) that can be applied at the pointcut of the input code. Moreover, one rule can be applied at several points of intersection. Connecting the board to the intersection point is called the “join point”.



Let's start with the SecurityAspect aspect code (SecurityAspect.aj file), which I used to implement the “check user rights” functionality:

package eu.vitaliy.testaspectjsecurity.aspects;

import eu.vitaliy.testaspectjsecurity.Allow;



public aspect SecurityAspect {

private SecurityAspectHelper helper

= new SecurityAspectHelper();



pointcut byMethod() : execution(@Allow * *.*(..));



pointcut byClass() : execution(* @Allow *.*(..))

&& !execution(@Allow * *.*(..));



before() : byMethod(){

helper.beforeMethod(thisJoinPoint);

}



before() : byClass(){

helper.beforeClass(thisJoinPoint);

}

}




Assistant class (I always place aspect logic in an extra class, aspect code becomes more transparent and easier to test):

package eu.vitaliy.testaspectjsecurity.aspects;



import java.lang.reflect.Method;



import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.reflect.MethodSignature;



import eu.vitaliy.testaspectjsecurity.Allow;

import eu.vitaliy.testaspectjsecurity.ERole;

import eu.vitaliy.testaspectjsecurity.PermissionStore;

import eu.vitaliy.testaspectjsecurity.MySecurityException;

enum ESecurytyType

{

CLASS, METHOD

}



public class SecurityAspectHelper {



void beforeMethod(JoinPoint pointcut)

{

before(pointcut, ESecurytyType.METHOD);

}



void beforeClass(JoinPoint pointcut)

{

before(pointcut, ESecurytyType.CLASS);

}

@SuppressWarnings( "unchecked" )

void before(JoinPoint pointcut, ESecurytyType securytyType)

{

MethodSignature methodSignature = (MethodSignature) pointcut.getSignature();

Method method = methodSignature.getMethod();

Class clazz = pointcut.getTarget().getClass();

Allow allow = null ;

if (securytyType == ESecurytyType.CLASS)

{

allow = (Allow) clazz.getAnnotation(Allow. class );

} else {

allow = method.getAnnotation(Allow. class );

}

ERole[] role = allow. value ();

for (ERole r : role)

{

if (!PermissionStore.check(r))

{

throw new MySecurityException( clazz.getName(), method.getName(), r);

}

}

}

}




* This source code was highlighted with Source Code Highlighter .


A little explanation about the SecurityAspect aspect:

pointcut is the “intersection point” of the code.

pointcut byMethod (): execution ( Allow * *. * (..));

- determines the point of intersection of any methods annotated using Allow



pointcut byClass (): execution (* Allow *. * (..)) &&! execution ( Allow * *. * (..));

- determines the point of intersection of any methods that are annotated in the class using Allow , but they themselves should not be annotated using Allow



You can make sure that the micro security framework code took about 45 lines.



To compile the program, I used the Eclipse 3.5.2 IDE with the AJDT 2.0.2 plugin installed, although this can be done using ant or maven



References:

Wikipedia article about aspect-oriented_programming

A working sample project for this article.

Source: https://habr.com/ru/post/95920/



All Articles