Как стать автором
Обновить

Декларативная система безопасности с помощью аннотаций и AspectJ в среде Java SE

Время на прочтение9 мин
Количество просмотров4.2K
Цель данной статьи — написание мини контейнера-фреймворка, который возьмет на себя задачи авторизации и аутентификации пользователя, позволяя писать минимум кода в клиентской программе. Сразу оговорюсь, в реальной жизни я использую Spring Security (ранее проект назывался Acegi).
Статья предназначена для тех кто хочет демистифицировать для себя магию работы подобных решений, либо для тех кто по каким либо причинам не считает целесообразным использование общедоступных решений и планирует создать собственную имплементацию системы безопасности. Одна из таких причин — ограниченный размер оперативной памяти и невозможность запуска «взрослых» фреймворков, например в такой Java среде как Android (у меня пока нет данных об успешном использовании AspectJ на платформе Android, но это лишь вопрос времени).

Не знаю как в других командах, у нас неохотно используют аннотации, хотя это нововведение было добавлено в язык Java в версии 1.5. Вроде как 7 лет прошло. ( и почти 10 лет после премьеры Microsoft.NET — там аннотации (атрибуты) были изначально). Признаюсь, я не знаю никого, кто бы писал их сам. В основном люди используют библиотеки, в которых они уже есть. Возможно потому, что не многие осознают, в каких ситуациях они действительно могут быть полезны. 7 лет достаточный срок чтобы понять — аннотации не являются однодневкой, криком моды. Похоже исчезать из Java они не собираются, да и из C# тоже. Думаю что они останутся в Java надолго.

У аннотаций существует существенный недостаток: для того чтобы во время выполнения контейнер (в простейшем случае какой нибудь метод, который возвращает созданный с помощью new объект, и делает перед этим некоторые действия ), смог найти аннотированный метод, параметр метода, или поле класса, он должен выполнить циклический просмотр всех методов и полей, проверяя аннотированы ли они. Это увеличивает время инициализации объекта и запуска программы. Как правило метаданные содержащиеся в аннотациях, которыми обладает конкретный тип кэшируют однако существует другой способ использовать аннотации существенно не увеличивая при этом время инициализации объекта.

Начнем с конечного результата. Для небольшого десктопного приложения, мне нужно было сделать систему безопасности — проверку прав пользователя для выполнения конкретных методов. К этому времени — после удачных опытов с EJB3, JBoss Seam и Spring — я уже успел привыкнуть к некоторым декларативным приёмам написания кода, не засоряя код программы разной служебной ерундой ( с точки зрения засорения основной логики программы ) типа проверки прав пользователя, определения границ трансакций и управления зависимостями-ссылками. Я решил, что также как в EJB3 и Spring Security в моей программе метод над которым стоит аннотация Allow(ERole.ADMIN) будет выполнен только если у пользователя (текущего потока выполнения ) есть права указанные в параметре аннотации Allow, то есть ERole.ADMIN. Для простоты примера права будут только двух видов: Allow(ERole.USER) и Allow(ERole.ADMIN).

Вот так будет выглядеть один из классов примера:
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()");
 }
}

Возможен альтернативный вариант, когда аннотируем весь класс. Тогда все методы класса нуждаются в соответствующих правах, и мы можем переопределить те методы, на выполнение которых требуются особые права.
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()");
  }
}

Вот так выглядит определение аннотации:
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};
}


Она зависит от перечисления ERole:
package eu.vitaliy.testaspectjsecurity;

public enum ERole {
  ADMIN,
  USER
}


В примере бедет используется простенькое хранилище PermissionStore в котором будут хранится права пользователя:

В примере используется простенькое хранилище PermissionStore в котором будут хранится права пользователя.Если у пользователя нет соответствующих прав будет сгенерировано исключение MySecurityException которое является наследником 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);
  }
}


А вот так выглядит метод main:
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();

  }
}


Для того чтобы программа выполнилась до конца пользователь должен иметь права ERole.USER и ERole.ADMIN

Теперь об имплементации. Как вы могли догадаться, код проверки прав пользователя, который выбрасывает MySecurityException, не сработает просто так. Традиционно объект, который конфигурируется с помощью аннотации создаётся с помощью фабрики-контейнера, потому что создавая объект с помощью оператора new пришлось бы произвести дополнительные действия для проверки прав пользователя. Технология которая поможет избежать сканирования класса на наличие аннотаций называется AspectJ, но сначала стоит сделать небольшой обзор как это делали раньше, без AspectJ. Если вам это не очень интересно, можете перейти к способу 3 — реализации с помощью AspectJ.

Способ 1 Java Reflection API (начиная с J2SE 1.3): Cоздать динамический прокси объект, который имплементирует те же интерфейсы что и наш класс с помощью java.lang reflect.Proxy и java.lang.reflect.InvocationHandler. В методе Object java.lang.reflect.InvocationHandler.invoke(Object proxy, Method method, Object[] args) проверить, аннотирован ли метод аннотацией Allow и в зависимости от содержимого value и некого PermissionStore ( там где храниться информация, какие права есть у пользователя ) выбросить (или нет) исключение MySecurutyException.
Недостатки этого способа:
1. Накладные расходы на создание объекта.
2. Накладные расходы на вызов каждого метода.
3. Класс обязан имплементировать кокой нибудь интерфейс.
4. Метод invoke класса InvocationHandler будет вызван для всех методов, в том числе для тех, которые не нуждаются в проверке безопасности. Если в классе небольшое число методов требующих проверки — ещё одни бесполезные накладные расходы.

Способ 2 библиотека CGLIB — библиотека для генерации байт кода во время выполнения.
Имеет существенно более быструю реализацию вызова методов прокси объекта. Принципиально не является чем то более революционным по сравнению с reflection. Недостатки те же что и для способа 1 за исключением того, что позволяет создать прокси объект, который не имплементирует никаких интерфейсов.

Способ 3 AspectJ — расширение языка Java которое добавляет в него конструкции аспектного программирования. Результатирующий код остаётся полностью совместимым со стандартной виртуальной машиной Java. AspectJ способен вставить проверочный код безопасности во время компиляции, находя интересующие нас методы по некоторой маске, которую мы можем задать, причём допускаются wildcards — символы используемый для замещения одного или нескольких других символов. Начиная с некоторой версии AspectJ, в маску может входить Java аннотация, что стало гораздо удобнее, так как раньше мне приходилось давать префиксы методам, чтобы к ним был применен advice (совет) безопасности например:
* *.secure*(..)
Как я уже сказал выше, проверочный код вставляется во время компиляции. Это выполняет компилятор aspectbench, входными данными которого является скомпилированный байт код, что является несомненным удобством, так как можно аспектировать коммерческий код без исходных кодов, в том числе для модификации логики его работы. Из за этого данный подход свободен от недостатков 1, 2 и 4 первого способа.

Основной синтаксической единицей языка AspectJ является аспект. Аспект — это набор правил(advice) которые могут быть применены в точках пересечения (pointcut) входного кода. Причём одно правило может быть применено в нескольких точках пересечения. Соединение совета с точкой пересечения называется «точкой соединения» (Joinpoint).

Начнём с кода аспекта SecurityAspect (файл SecurityAspect.aj), который я использовал для реализации функциональности «проверь права пользователя»:
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);
  }
}


Класс помощник (Я всегда размещаю логику аспекта в дополнительном классе, код аспекта становится более прозрачным и проще тестируется ):
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.

Небольшие пояснения по поводу аспекта SecurityAspect:
pointcut это «точка пересечения» кода.
pointcut byMethod(): execution( Allow * *.*(..));
— определяет точку пересечения любых методов аннотированых с помощью Allow

pointcut byClass(): execution(* Allow *.*(..)) && !execution( Allow * *.*(..));
— определяет точку пересечения любых методов находящихся в классе аннотированом с помощью Allow, но при этом сами они не должны быть аннотированы с помощью Allow

Можно убедиться, что код «микро фреймворка безопасности» занял около 45 строк

Для компиляции программы я использовал IDE Eclipse 3.5.2 с установленным плагином AJDT 2.0.2 хотя это можно сделать с помощью ant или maven

Ссылки:
Статья в википедии про аспектно-ориентированное_программирование
Работающий пример проекта для этой статьи
Теги:
Хабы:
+8
Комментарии0

Публикации

Истории

Работа

Java разработчик
358 вакансий

Ближайшие события